packages feed

notion-client 0.5.0.0 → 0.6.0.0

raw patch · 13 files changed

+1983/−170 lines, 13 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Notion.V1.Blocks: BlockContent :: Value -> BlockContent
- Notion.V1.Blocks: instance Data.Aeson.Types.ToJSON.ToJSON Notion.V1.Blocks.BlockContent
- Notion.V1.Blocks: instance GHC.Internal.Generics.Generic Notion.V1.Blocks.BlockContent
- Notion.V1.Blocks: instance GHC.Internal.Show.Show Notion.V1.Blocks.BlockContent
- Notion.V1.Blocks: newtype BlockContent
+ Notion.V1.BlockContent: Abap :: CodeLanguage
+ Notion.V1.BlockContent: Arduino :: CodeLanguage
+ Notion.V1.BlockContent: AudioBlock :: FileSource -> BlockContent
+ Notion.V1.BlockContent: Bash :: CodeLanguage
+ Notion.V1.BlockContent: Basic :: CodeLanguage
+ Notion.V1.BlockContent: BlockUpdate :: BlockContent -> BlockUpdate
+ Notion.V1.BlockContent: BookmarkBlock :: Text -> Vector RichText -> BlockContent
+ Notion.V1.BlockContent: BreadcrumbBlock :: BlockContent
+ Notion.V1.BlockContent: BulletedListItemBlock :: Vector RichText -> Color -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: C :: CodeLanguage
+ Notion.V1.BlockContent: CSharp :: CodeLanguage
+ Notion.V1.BlockContent: CalloutBlock :: Vector RichText -> Color -> Maybe Icon -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: ChildDatabaseBlock :: Text -> BlockContent
+ Notion.V1.BlockContent: ChildPageBlock :: Text -> BlockContent
+ Notion.V1.BlockContent: Clojure :: CodeLanguage
+ Notion.V1.BlockContent: CodeBlock :: Vector RichText -> Vector RichText -> CodeLanguage -> BlockContent
+ Notion.V1.BlockContent: CoffeeScript :: CodeLanguage
+ Notion.V1.BlockContent: ColumnBlock :: Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: ColumnListBlock :: Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: Cpp :: CodeLanguage
+ Notion.V1.BlockContent: Css :: CodeLanguage
+ Notion.V1.BlockContent: Dart :: CodeLanguage
+ Notion.V1.BlockContent: Diff :: CodeLanguage
+ Notion.V1.BlockContent: DividerBlock :: BlockContent
+ Notion.V1.BlockContent: Docker :: CodeLanguage
+ Notion.V1.BlockContent: Elixir :: CodeLanguage
+ Notion.V1.BlockContent: Elm :: CodeLanguage
+ Notion.V1.BlockContent: EmbedBlock :: Text -> BlockContent
+ Notion.V1.BlockContent: EquationBlock :: Text -> BlockContent
+ Notion.V1.BlockContent: Erlang :: CodeLanguage
+ Notion.V1.BlockContent: ExternalSource :: ExternalFile -> FileSource
+ Notion.V1.BlockContent: FSharp :: CodeLanguage
+ Notion.V1.BlockContent: FileBlock :: FileSource -> Vector RichText -> Maybe Text -> BlockContent
+ Notion.V1.BlockContent: FileUploadSource :: UUID -> FileSource
+ Notion.V1.BlockContent: Flow :: CodeLanguage
+ Notion.V1.BlockContent: Fortran :: CodeLanguage
+ Notion.V1.BlockContent: Gherkin :: CodeLanguage
+ Notion.V1.BlockContent: Glsl :: CodeLanguage
+ Notion.V1.BlockContent: Go :: CodeLanguage
+ Notion.V1.BlockContent: GraphQL :: CodeLanguage
+ Notion.V1.BlockContent: Groovy :: CodeLanguage
+ Notion.V1.BlockContent: Haskell :: CodeLanguage
+ Notion.V1.BlockContent: Heading1Block :: Vector RichText -> Color -> Bool -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: Heading2Block :: Vector RichText -> Color -> Bool -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: Heading3Block :: Vector RichText -> Color -> Bool -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: Html :: CodeLanguage
+ Notion.V1.BlockContent: ImageBlock :: FileSource -> Vector RichText -> BlockContent
+ Notion.V1.BlockContent: Java :: CodeLanguage
+ Notion.V1.BlockContent: JavaCCppCSharp :: CodeLanguage
+ Notion.V1.BlockContent: JavaScript :: CodeLanguage
+ Notion.V1.BlockContent: Json :: CodeLanguage
+ Notion.V1.BlockContent: Julia :: CodeLanguage
+ Notion.V1.BlockContent: Kotlin :: CodeLanguage
+ Notion.V1.BlockContent: LaTeX :: CodeLanguage
+ Notion.V1.BlockContent: Less :: CodeLanguage
+ Notion.V1.BlockContent: Letters :: ListFormat
+ Notion.V1.BlockContent: LinkPreviewBlock :: Text -> BlockContent
+ Notion.V1.BlockContent: LinkToComment :: UUID -> LinkTarget
+ Notion.V1.BlockContent: LinkToDatabase :: UUID -> LinkTarget
+ Notion.V1.BlockContent: LinkToPage :: UUID -> LinkTarget
+ Notion.V1.BlockContent: LinkToPageBlock :: LinkTarget -> BlockContent
+ Notion.V1.BlockContent: Lisp :: CodeLanguage
+ Notion.V1.BlockContent: LiveScript :: CodeLanguage
+ Notion.V1.BlockContent: Lua :: CodeLanguage
+ Notion.V1.BlockContent: Makefile :: CodeLanguage
+ Notion.V1.BlockContent: Markdown :: CodeLanguage
+ Notion.V1.BlockContent: Markup :: CodeLanguage
+ Notion.V1.BlockContent: Matlab :: CodeLanguage
+ Notion.V1.BlockContent: Mermaid :: CodeLanguage
+ Notion.V1.BlockContent: Nix :: CodeLanguage
+ Notion.V1.BlockContent: NotionSource :: File -> FileSource
+ Notion.V1.BlockContent: NumberedListItemBlock :: Vector RichText -> Color -> Maybe ListFormat -> Maybe Natural -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: Numbers :: ListFormat
+ Notion.V1.BlockContent: OCaml :: CodeLanguage
+ Notion.V1.BlockContent: ObjectiveC :: CodeLanguage
+ Notion.V1.BlockContent: ParagraphBlock :: Vector RichText -> Color -> Maybe Icon -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: Pascal :: CodeLanguage
+ Notion.V1.BlockContent: PdfBlock :: FileSource -> Vector RichText -> BlockContent
+ Notion.V1.BlockContent: Perl :: CodeLanguage
+ Notion.V1.BlockContent: Php :: CodeLanguage
+ Notion.V1.BlockContent: PlainText :: CodeLanguage
+ Notion.V1.BlockContent: PowerShell :: CodeLanguage
+ Notion.V1.BlockContent: Prolog :: CodeLanguage
+ Notion.V1.BlockContent: Protobuf :: CodeLanguage
+ Notion.V1.BlockContent: Python :: CodeLanguage
+ Notion.V1.BlockContent: QuoteBlock :: Vector RichText -> Color -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: R :: CodeLanguage
+ Notion.V1.BlockContent: Reason :: CodeLanguage
+ Notion.V1.BlockContent: Roman :: ListFormat
+ Notion.V1.BlockContent: Ruby :: CodeLanguage
+ Notion.V1.BlockContent: Rust :: CodeLanguage
+ Notion.V1.BlockContent: Sass :: CodeLanguage
+ Notion.V1.BlockContent: Scala :: CodeLanguage
+ Notion.V1.BlockContent: Scheme :: CodeLanguage
+ Notion.V1.BlockContent: Scss :: CodeLanguage
+ Notion.V1.BlockContent: Shell :: CodeLanguage
+ Notion.V1.BlockContent: Sql :: CodeLanguage
+ Notion.V1.BlockContent: Swift :: CodeLanguage
+ Notion.V1.BlockContent: SyncedBlockContent :: SyncedFrom -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: SyncedOriginal :: SyncedFrom
+ Notion.V1.BlockContent: SyncedReference :: UUID -> SyncedFrom
+ Notion.V1.BlockContent: TableBlock :: Natural -> Bool -> Bool -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: TableOfContentsBlock :: Color -> BlockContent
+ Notion.V1.BlockContent: TableRowBlock :: Vector (Vector RichText) -> BlockContent
+ Notion.V1.BlockContent: ToDoBlock :: Vector RichText -> Color -> Bool -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: ToggleBlock :: Vector RichText -> Color -> Vector BlockContent -> BlockContent
+ Notion.V1.BlockContent: TypeScript :: CodeLanguage
+ Notion.V1.BlockContent: UnknownBlock :: Text -> Value -> BlockContent
+ Notion.V1.BlockContent: UnsupportedBlock :: BlockContent
+ Notion.V1.BlockContent: VbNet :: CodeLanguage
+ Notion.V1.BlockContent: Verilog :: CodeLanguage
+ Notion.V1.BlockContent: Vhdl :: CodeLanguage
+ Notion.V1.BlockContent: VideoBlock :: FileSource -> Vector RichText -> BlockContent
+ Notion.V1.BlockContent: VisualBasic :: CodeLanguage
+ Notion.V1.BlockContent: WebAssembly :: CodeLanguage
+ Notion.V1.BlockContent: Xml :: CodeLanguage
+ Notion.V1.BlockContent: Yaml :: CodeLanguage
+ Notion.V1.BlockContent: [audioSource] :: BlockContent -> FileSource
+ Notion.V1.BlockContent: [calloutIcon] :: BlockContent -> Maybe Icon
+ Notion.V1.BlockContent: [caption] :: BlockContent -> Vector RichText
+ Notion.V1.BlockContent: [cells] :: BlockContent -> Vector (Vector RichText)
+ Notion.V1.BlockContent: [checked] :: BlockContent -> Bool
+ Notion.V1.BlockContent: [children] :: BlockContent -> Vector BlockContent
+ Notion.V1.BlockContent: [color] :: BlockContent -> Color
+ Notion.V1.BlockContent: [expression] :: BlockContent -> Text
+ Notion.V1.BlockContent: [fileName] :: BlockContent -> Maybe Text
+ Notion.V1.BlockContent: [fileSource] :: BlockContent -> FileSource
+ Notion.V1.BlockContent: [hasColumnHeader] :: BlockContent -> Bool
+ Notion.V1.BlockContent: [hasRowHeader] :: BlockContent -> Bool
+ Notion.V1.BlockContent: [imageSource] :: BlockContent -> FileSource
+ Notion.V1.BlockContent: [isToggleable] :: BlockContent -> Bool
+ Notion.V1.BlockContent: [language] :: BlockContent -> CodeLanguage
+ Notion.V1.BlockContent: [linkTarget] :: BlockContent -> LinkTarget
+ Notion.V1.BlockContent: [listFormat] :: BlockContent -> Maybe ListFormat
+ Notion.V1.BlockContent: [listStartIndex] :: BlockContent -> Maybe Natural
+ Notion.V1.BlockContent: [paragraphIcon] :: BlockContent -> Maybe Icon
+ Notion.V1.BlockContent: [pdfSource] :: BlockContent -> FileSource
+ Notion.V1.BlockContent: [richText] :: BlockContent -> Vector RichText
+ Notion.V1.BlockContent: [syncedFrom] :: BlockContent -> SyncedFrom
+ Notion.V1.BlockContent: [tableWidth] :: BlockContent -> Natural
+ Notion.V1.BlockContent: [title] :: BlockContent -> Text
+ Notion.V1.BlockContent: [url] :: BlockContent -> Text
+ Notion.V1.BlockContent: [videoSource] :: BlockContent -> FileSource
+ Notion.V1.BlockContent: blockContentFields :: BlockContent -> (Text, Value)
+ Notion.V1.BlockContent: blockContentType :: BlockContent -> Text
+ Notion.V1.BlockContent: bookmarkBlock :: Text -> BlockContent
+ Notion.V1.BlockContent: bulletedListItemBlock :: Vector RichText -> BlockContent
+ Notion.V1.BlockContent: calloutBlock :: Vector RichText -> Maybe Icon -> BlockContent
+ Notion.V1.BlockContent: codeBlock :: Vector RichText -> CodeLanguage -> BlockContent
+ Notion.V1.BlockContent: data BlockContent
+ Notion.V1.BlockContent: data CodeLanguage
+ Notion.V1.BlockContent: data FileSource
+ Notion.V1.BlockContent: data LinkTarget
+ Notion.V1.BlockContent: data ListFormat
+ Notion.V1.BlockContent: data SyncedFrom
+ Notion.V1.BlockContent: dividerBlock :: BlockContent
+ Notion.V1.BlockContent: equationBlock :: Text -> BlockContent
+ Notion.V1.BlockContent: headingBlock :: Int -> Vector RichText -> BlockContent
+ Notion.V1.BlockContent: imageBlock :: FileSource -> BlockContent
+ Notion.V1.BlockContent: instance Data.Aeson.Types.FromJSON.FromJSON Notion.V1.BlockContent.BlockContent
+ Notion.V1.BlockContent: instance Data.Aeson.Types.FromJSON.FromJSON Notion.V1.BlockContent.CodeLanguage
+ Notion.V1.BlockContent: instance Data.Aeson.Types.FromJSON.FromJSON Notion.V1.BlockContent.FileSource
+ Notion.V1.BlockContent: instance Data.Aeson.Types.FromJSON.FromJSON Notion.V1.BlockContent.LinkTarget
+ Notion.V1.BlockContent: instance Data.Aeson.Types.FromJSON.FromJSON Notion.V1.BlockContent.ListFormat
+ Notion.V1.BlockContent: instance Data.Aeson.Types.FromJSON.FromJSON Notion.V1.BlockContent.SyncedFrom
+ Notion.V1.BlockContent: instance Data.Aeson.Types.ToJSON.ToJSON Notion.V1.BlockContent.BlockContent
+ Notion.V1.BlockContent: instance Data.Aeson.Types.ToJSON.ToJSON Notion.V1.BlockContent.BlockUpdate
+ Notion.V1.BlockContent: instance Data.Aeson.Types.ToJSON.ToJSON Notion.V1.BlockContent.CodeLanguage
+ Notion.V1.BlockContent: instance Data.Aeson.Types.ToJSON.ToJSON Notion.V1.BlockContent.FileSource
+ Notion.V1.BlockContent: instance Data.Aeson.Types.ToJSON.ToJSON Notion.V1.BlockContent.LinkTarget
+ Notion.V1.BlockContent: instance Data.Aeson.Types.ToJSON.ToJSON Notion.V1.BlockContent.ListFormat
+ Notion.V1.BlockContent: instance Data.Aeson.Types.ToJSON.ToJSON Notion.V1.BlockContent.SyncedFrom
+ Notion.V1.BlockContent: instance GHC.Classes.Eq Notion.V1.BlockContent.BlockContent
+ Notion.V1.BlockContent: instance GHC.Classes.Eq Notion.V1.BlockContent.CodeLanguage
+ Notion.V1.BlockContent: instance GHC.Classes.Eq Notion.V1.BlockContent.FileSource
+ Notion.V1.BlockContent: instance GHC.Classes.Eq Notion.V1.BlockContent.LinkTarget
+ Notion.V1.BlockContent: instance GHC.Classes.Eq Notion.V1.BlockContent.ListFormat
+ Notion.V1.BlockContent: instance GHC.Classes.Eq Notion.V1.BlockContent.SyncedFrom
+ Notion.V1.BlockContent: instance GHC.Internal.Generics.Generic Notion.V1.BlockContent.BlockContent
+ Notion.V1.BlockContent: instance GHC.Internal.Generics.Generic Notion.V1.BlockContent.CodeLanguage
+ Notion.V1.BlockContent: instance GHC.Internal.Generics.Generic Notion.V1.BlockContent.ListFormat
+ Notion.V1.BlockContent: instance GHC.Internal.Show.Show Notion.V1.BlockContent.BlockContent
+ Notion.V1.BlockContent: instance GHC.Internal.Show.Show Notion.V1.BlockContent.BlockUpdate
+ Notion.V1.BlockContent: instance GHC.Internal.Show.Show Notion.V1.BlockContent.CodeLanguage
+ Notion.V1.BlockContent: instance GHC.Internal.Show.Show Notion.V1.BlockContent.FileSource
+ Notion.V1.BlockContent: instance GHC.Internal.Show.Show Notion.V1.BlockContent.LinkTarget
+ Notion.V1.BlockContent: instance GHC.Internal.Show.Show Notion.V1.BlockContent.ListFormat
+ Notion.V1.BlockContent: instance GHC.Internal.Show.Show Notion.V1.BlockContent.SyncedFrom
+ Notion.V1.BlockContent: mkRichText :: Text -> Vector RichText
+ Notion.V1.BlockContent: newtype BlockUpdate
+ Notion.V1.BlockContent: numberedListItemBlock :: Vector RichText -> BlockContent
+ Notion.V1.BlockContent: paragraphBlock :: Vector RichText -> BlockContent
+ Notion.V1.BlockContent: parseBlockContent :: Text -> Value -> Parser BlockContent
+ Notion.V1.BlockContent: quoteBlock :: Vector RichText -> BlockContent
+ Notion.V1.BlockContent: textBlock :: Text -> BlockContent
+ Notion.V1.BlockContent: toDoBlock :: Vector RichText -> Bool -> BlockContent
+ Notion.V1.BlockContent: toggleBlock :: Vector RichText -> BlockContent
+ Notion.V1.BlockContent: withChildren :: BlockContent -> Vector BlockContent -> BlockContent
+ Notion.V1.Blocks: BlockUpdate :: BlockContent -> BlockUpdate
+ Notion.V1.Blocks: newtype BlockUpdate
+ Notion.V1.Common: instance GHC.Classes.Eq Notion.V1.Common.Cover
+ Notion.V1.Common: instance GHC.Classes.Eq Notion.V1.Common.ExternalFile
+ Notion.V1.Common: instance GHC.Classes.Eq Notion.V1.Common.File
+ Notion.V1.Common: instance GHC.Classes.Eq Notion.V1.Common.Icon
+ Notion.V1.RichText: instance GHC.Classes.Eq Notion.V1.RichText.Annotations
+ Notion.V1.RichText: instance GHC.Classes.Eq Notion.V1.RichText.Date
+ Notion.V1.RichText: instance GHC.Classes.Eq Notion.V1.RichText.EquationContent
+ Notion.V1.RichText: instance GHC.Classes.Eq Notion.V1.RichText.Link
+ Notion.V1.RichText: instance GHC.Classes.Eq Notion.V1.RichText.MentionContent
+ Notion.V1.RichText: instance GHC.Classes.Eq Notion.V1.RichText.RichText
+ Notion.V1.RichText: instance GHC.Classes.Eq Notion.V1.RichText.RichTextContent
+ Notion.V1.RichText: instance GHC.Classes.Eq Notion.V1.RichText.TextContent
+ Notion.V1.RichText: instance GHC.Classes.Eq Notion.V1.RichText.TimeZone
- Notion.V1: Methods :: (CreateDatabase -> IO DatabaseObject) -> (DatabaseID -> IO DatabaseObject) -> (DatabaseID -> UpdateDatabase -> IO DatabaseObject) -> (DatabaseID -> QueryDatabase -> IO (ListOf PageObject)) -> (DataSourceID -> IO DataSourceObject) -> (CreateDataSource -> IO DataSourceObject) -> (DataSourceID -> UpdateDataSource -> IO DataSourceObject) -> (DataSourceID -> QueryDataSource -> IO (ListOf PageObject)) -> (DataSourceID -> Maybe Text -> Maybe Text -> Maybe Natural -> IO ListTemplatesResponse) -> (CreatePage -> IO PageObject) -> (PageID -> IO PageObject) -> (PageID -> UpdatePage -> IO PageObject) -> (PageID -> Text -> Maybe Text -> Maybe Natural -> IO PropertyItemResponse) -> (PageID -> Maybe Bool -> IO PageMarkdown) -> (PageID -> UpdatePageMarkdown -> IO PageMarkdown) -> (PageID -> MovePage -> IO PageObject) -> (BlockID -> IO BlockObject) -> (BlockID -> BlockContent -> IO BlockObject) -> (ParentID -> Maybe Natural -> Maybe Text -> IO (ListOf BlockObject)) -> (ParentID -> AppendBlockChildren -> IO (ListOf BlockObject)) -> (BlockID -> IO BlockObject) -> (UserID -> IO UserObject) -> (Maybe Natural -> Maybe Text -> IO (ListOf UserObject)) -> IO UserObject -> (SearchRequest -> IO (ListOf Value)) -> (CreateComment -> IO CommentObject) -> (Maybe BlockID -> Maybe Text -> Maybe Natural -> IO (ListOf CommentObject)) -> (CreateView -> IO ViewObject) -> (ViewID -> IO ViewObject) -> (ViewID -> UpdateView -> IO ViewObject) -> (ViewID -> IO ViewObject) -> (Maybe UUID -> Maybe UUID -> Maybe Text -> Maybe Natural -> IO (ListOf ViewObject)) -> (ViewID -> QueryView -> IO (ListOf PageObject)) -> (Maybe Text -> Maybe Text -> Maybe Natural -> IO (ListOf CustomEmoji)) -> Methods
+ Notion.V1: Methods :: (CreateDatabase -> IO DatabaseObject) -> (DatabaseID -> IO DatabaseObject) -> (DatabaseID -> UpdateDatabase -> IO DatabaseObject) -> (DatabaseID -> QueryDatabase -> IO (ListOf PageObject)) -> (DataSourceID -> IO DataSourceObject) -> (CreateDataSource -> IO DataSourceObject) -> (DataSourceID -> UpdateDataSource -> IO DataSourceObject) -> (DataSourceID -> QueryDataSource -> IO (ListOf PageObject)) -> (DataSourceID -> Maybe Text -> Maybe Text -> Maybe Natural -> IO ListTemplatesResponse) -> (CreatePage -> IO PageObject) -> (PageID -> IO PageObject) -> (PageID -> UpdatePage -> IO PageObject) -> (PageID -> Text -> Maybe Text -> Maybe Natural -> IO PropertyItemResponse) -> (PageID -> Maybe Bool -> IO PageMarkdown) -> (PageID -> UpdatePageMarkdown -> IO PageMarkdown) -> (PageID -> MovePage -> IO PageObject) -> (BlockID -> IO BlockObject) -> (BlockID -> BlockUpdate -> IO BlockObject) -> (ParentID -> Maybe Natural -> Maybe Text -> IO (ListOf BlockObject)) -> (ParentID -> AppendBlockChildren -> IO (ListOf BlockObject)) -> (BlockID -> IO BlockObject) -> (UserID -> IO UserObject) -> (Maybe Natural -> Maybe Text -> IO (ListOf UserObject)) -> IO UserObject -> (SearchRequest -> IO (ListOf Value)) -> (CreateComment -> IO CommentObject) -> (Maybe BlockID -> Maybe Text -> Maybe Natural -> IO (ListOf CommentObject)) -> (CreateView -> IO ViewObject) -> (ViewID -> IO ViewObject) -> (ViewID -> UpdateView -> IO ViewObject) -> (ViewID -> IO ViewObject) -> (Maybe UUID -> Maybe UUID -> Maybe Text -> Maybe Natural -> IO (ListOf ViewObject)) -> (ViewID -> QueryView -> IO (ListOf PageObject)) -> (Maybe Text -> Maybe Text -> Maybe Natural -> IO (ListOf CustomEmoji)) -> Methods
- Notion.V1: [updateBlock] :: Methods -> BlockID -> BlockContent -> IO BlockObject
+ Notion.V1: [updateBlock] :: Methods -> BlockID -> BlockUpdate -> IO BlockObject
- Notion.V1.Blocks: AppendBlockChildren :: Vector Value -> Maybe Position -> AppendBlockChildren
+ Notion.V1.Blocks: AppendBlockChildren :: Vector BlockContent -> Maybe Position -> AppendBlockChildren
- Notion.V1.Blocks: BlockObject :: BlockID -> Parent -> POSIXTime -> POSIXTime -> UserReference -> UserReference -> Bool -> Bool -> Text -> Value -> ObjectType -> BlockObject
+ Notion.V1.Blocks: BlockObject :: BlockID -> Parent -> POSIXTime -> POSIXTime -> UserReference -> UserReference -> Bool -> Bool -> Text -> BlockContent -> ObjectType -> BlockObject
- Notion.V1.Blocks: [children] :: AppendBlockChildren -> Vector Value
+ Notion.V1.Blocks: [children] :: AppendBlockChildren -> Vector BlockContent
- Notion.V1.Blocks: [content] :: BlockContent -> Value
+ Notion.V1.Blocks: [content] :: BlockObject -> BlockContent
- Notion.V1.Blocks: type API = "blocks" :> Capture "block_id" BlockID :> Get '[JSON] BlockObject :<|> Capture "block_id" BlockID :> ReqBody '[JSON] BlockContent :> Patch '[JSON] BlockObject :<|> Capture "block_id" BlockID :> "children" :> QueryParam "page_size" Natural :> QueryParam "start_cursor" Text :> Get '[JSON] ListOf BlockObject :<|> Capture "block_id" BlockID :> "children" :> ReqBody '[JSON] AppendBlockChildren :> Patch '[JSON] ListOf BlockObject :<|> Capture "block_id" BlockID :> Delete '[JSON] BlockObject
+ Notion.V1.Blocks: type API = "blocks" :> Capture "block_id" BlockID :> Get '[JSON] BlockObject :<|> Capture "block_id" BlockID :> ReqBody '[JSON] BlockUpdate :> Patch '[JSON] BlockObject :<|> Capture "block_id" BlockID :> "children" :> QueryParam "page_size" Natural :> QueryParam "start_cursor" Text :> Get '[JSON] ListOf BlockObject :<|> Capture "block_id" BlockID :> "children" :> ReqBody '[JSON] AppendBlockChildren :> Patch '[JSON] ListOf BlockObject :<|> Capture "block_id" BlockID :> Delete '[JSON] BlockObject
- Notion.V1.Pages: CreatePage :: Parent -> PageProperties -> Maybe (Vector Value) -> Maybe Text -> Maybe Icon -> Maybe Cover -> Maybe Template -> Maybe Position -> CreatePage
+ Notion.V1.Pages: CreatePage :: Parent -> PageProperties -> Maybe (Vector BlockContent) -> Maybe Text -> Maybe Icon -> Maybe Cover -> Maybe Template -> Maybe Position -> CreatePage
- Notion.V1.Pages: [children] :: CreatePage -> Maybe (Vector Value)
+ Notion.V1.Pages: [children] :: CreatePage -> Maybe (Vector BlockContent)

Files

CHANGELOG.md view
@@ -1,5 +1,16 @@ # Changelog for notion-client +## 0.6.0.0 (2026-03-30)++### Breaking Changes+* Replace untyped `Value` block content with typed `BlockContent` sum type in `Notion.V1.BlockContent` module+* `AppendBlockChildren` and `CreatePage` now use `[BlockContent]` instead of `[Value]` for children+* Block types are now statically typed variants (e.g., `Paragraph`, `Heading1`, `BulletedListItem`, `Code`, `Toggle`, `Callout`, etc.) instead of free-form JSON++### New Features+* **Typed Block Content**: New `Notion.V1.BlockContent` module with `BlockContent` sum type for creating blocks with compile-time safety and smart constructors+* **Nested Block Children**: `BlockContent` supports recursive `children` for creating nested block hierarchies in a single API call (e.g., toggles containing paragraphs, bulleted lists with sub-items)+ ## 0.5.0.0 (2026-03-29)  ### Breaking Changes
+ notion-client-example/BlockDemo.hs view
@@ -0,0 +1,616 @@+-- |+-- Block API demonstration showcasing typed block content.+--+-- Demonstrates creating various block types using the typed smart constructors+-- from "Notion.V1.BlockContent" instead of raw JSON.+module BlockDemo+  ( runBlockDemo,+  )+where++import Console (printHeader, runTest)+import Data.String (fromString)+import Data.Text qualified as Text+import Data.Vector qualified as Vector+import Notion.V1 (Methods (..))+import Notion.V1.BlockContent+import Notion.V1.Blocks (AppendBlockChildren (..), Position (..))+import Notion.V1.Blocks qualified as Blocks+import Notion.V1.Common (Color (..), ExternalFile (..), Icon (..), Parent (..))+import Notion.V1.ListOf (ListOf (..))+import Notion.V1.Pages (CreatePage (..), PageObject (..), mkCreatePage)+import Notion.V1.RichText (Annotations (..), RichText (..), RichTextContent (..), TextContent (..), defaultAnnotations)+import Prelude hiding (id)++-- | Run the Block API demonstration+runBlockDemo :: Methods -> String -> IO ()+runBlockDemo methods pageIdStr = do+  let parentPageId = fromString pageIdStr++  printHeader (Text.pack "Typed Block Content Demo")+  putStrLn "Demonstrating typed block creation using smart constructors"++  -- Create a test page for block demos+  let createReq = mkCreatePage (PageParent {pageId = parentPageId}) mempty+  page <-+    runTest (Text.pack "Creating test page for block demo") $+      createPage methods createReq {children = Nothing, icon = Just (EmojiIcon "🧱")}+  let PageObject {id = testPageId} = page+  putStrLn $ "Test page created: " <> show testPageId++  -- -----------------------------------------------------------------------+  -- Section 1: Text blocks using smart constructors+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "Text Blocks (Smart Constructors)")++  let textBlocks =+        Vector.fromList+          [ -- Simple paragraph from plain text+            textBlock "This paragraph was created with textBlock — the simplest way to add text.",+            -- Paragraph with rich text+            paragraphBlock (mkRichText "This paragraph uses paragraphBlock with mkRichText."),+            -- Headings at all three levels+            headingBlock 1 (mkRichText "Heading Level 1"),+            headingBlock 2 (mkRichText "Heading Level 2"),+            headingBlock 3 (mkRichText "Heading Level 3"),+            -- List items+            bulletedListItemBlock (mkRichText "First bulleted item"),+            bulletedListItemBlock (mkRichText "Second bulleted item"),+            numberedListItemBlock (mkRichText "First numbered item"),+            numberedListItemBlock (mkRichText "Second numbered item"),+            -- To-do items+            toDoBlock (mkRichText "Unchecked task") False,+            toDoBlock (mkRichText "Completed task") True,+            -- Toggle+            toggleBlock (mkRichText "Click to expand this toggle"),+            -- Quote+            quoteBlock (mkRichText "This is a quote block — great for callouts or citations."),+            -- Divider+            dividerBlock+          ]+      appendReq1 = AppendBlockChildren {children = textBlocks, position = Nothing}++  result1 <-+    runTest (Text.pack "Appending text blocks") $+      appendBlockChildren methods testPageId appendReq1+  putStrLn $ "Appended " <> show (Vector.length $ results result1) <> " text blocks"++  -- -----------------------------------------------------------------------+  -- Section 2: Code, equation, and callout blocks+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "Code, Equation, and Callout Blocks")++  let specialBlocks =+        Vector.fromList+          [ headingBlock 2 (mkRichText "Code Examples"),+            -- Code block with Haskell+            codeBlock+              (mkRichText "main :: IO ()\nmain = putStrLn \"Hello from typed blocks!\"")+              Haskell,+            -- Code block with JavaScript+            codeBlock+              (mkRichText "const greet = (name) => `Hello, ${name}!`;")+              JavaScript,+            -- Code block with Python+            codeBlock+              (mkRichText "def factorial(n):\n    return 1 if n <= 1 else n * factorial(n - 1)")+              Python,+            -- Equation block+            equationBlock "E = mc^2",+            -- Callout with emoji icon+            calloutBlock+              (mkRichText "This is a callout with an emoji icon")+              (Just (EmojiIcon "💡")),+            -- Callout without icon+            calloutBlock+              (mkRichText "This is a callout without an icon")+              Nothing+          ]+      appendReq2 = AppendBlockChildren {children = specialBlocks, position = Nothing}++  result2 <-+    runTest (Text.pack "Appending code/equation/callout blocks") $+      appendBlockChildren methods testPageId appendReq2+  putStrLn $ "Appended " <> show (Vector.length $ results result2) <> " special blocks"++  -- -----------------------------------------------------------------------+  -- Section 3: Embeds and media+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "Embed and Bookmark Blocks")++  let embedBlocks =+        Vector.fromList+          [ headingBlock 2 (mkRichText "Links and Embeds"),+            -- Bookmark+            bookmarkBlock "https://www.haskell.org",+            -- External image+            imageBlock (ExternalSource (ExternalFile {url = "https://www.haskell.org/img/haskell-logo.svg"})),+            dividerBlock+          ]+      appendReq3 = AppendBlockChildren {children = embedBlocks, position = Nothing}++  result3 <-+    runTest (Text.pack "Appending embed/bookmark blocks") $+      appendBlockChildren methods testPageId appendReq3+  putStrLn $ "Appended " <> show (Vector.length $ results result3) <> " embed blocks"++  -- -----------------------------------------------------------------------+  -- Section 4: Blocks with full constructor control (not smart constructors)+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "Blocks with Full Control")++  let -- Rich text with annotations+      boldRichText =+        Vector.singleton+          RichText+            { plainText = "Bold and colored text",+              href = Nothing,+              annotations = defaultAnnotations {bold = True},+              type_ = "text",+              content = TextContentWrapper (TextContent {content = "Bold and colored text", link = Nothing})+            }++      -- Paragraph with non-default color+      coloredParagraph = ParagraphBlock boldRichText Blue Nothing Vector.empty++      -- Toggleable heading+      toggleableHeading = Heading2Block (mkRichText "This heading is toggleable") Default True Vector.empty++      fullControlBlocks =+        Vector.fromList+          [ headingBlock 2 (mkRichText "Full Constructor Control"),+            coloredParagraph,+            toggleableHeading,+            dividerBlock+          ]+      appendReq4 = AppendBlockChildren {children = fullControlBlocks, position = Nothing}++  result4 <-+    runTest (Text.pack "Appending blocks with full control") $+      appendBlockChildren methods testPageId appendReq4+  putStrLn $ "Appended " <> show (Vector.length $ results result4) <> " blocks with full control"++  -- -----------------------------------------------------------------------+  -- Section 5: Read blocks back and pattern match on typed content+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "Reading and Pattern Matching")++  allBlocks <-+    runTest (Text.pack "Retrieving all blocks from test page") $+      listBlockChildren methods testPageId Nothing Nothing+  let List {results = allBlockResults} = allBlocks+  putStrLn $ "Total blocks on page: " <> show (Vector.length allBlockResults)++  -- Demonstrate pattern matching on typed content+  putStrLn "\nBlock types found (via pattern matching):"+  Vector.forM_ allBlockResults $ \block -> do+    let prefix = "  - "+    case Blocks.content block of+      ParagraphBlock {} -> putStrLn $ prefix <> "paragraph"+      Heading1Block {} -> putStrLn $ prefix <> "heading_1"+      Heading2Block {isToggleable} ->+        putStrLn $ prefix <> "heading_2" <> if isToggleable then " (toggleable)" else ""+      Heading3Block {} -> putStrLn $ prefix <> "heading_3"+      BulletedListItemBlock {} -> putStrLn $ prefix <> "bulleted_list_item"+      NumberedListItemBlock {listFormat} ->+        putStrLn $ prefix <> "numbered_list_item" <> maybe "" (\f -> " (format: " <> show f <> ")") listFormat+      ToDoBlock {checked} ->+        putStrLn $ prefix <> "to_do [" <> (if checked then "x" else " ") <> "]"+      ToggleBlock {} -> putStrLn $ prefix <> "toggle"+      QuoteBlock {} -> putStrLn $ prefix <> "quote"+      CalloutBlock {calloutIcon} ->+        putStrLn $ prefix <> "callout" <> maybe "" (\_ -> " (with icon)") calloutIcon+      CodeBlock {language} ->+        putStrLn $ prefix <> "code (" <> show language <> ")"+      EquationBlock {expression} ->+        putStrLn $ prefix <> "equation: " <> Text.unpack expression+      ImageBlock {} -> putStrLn $ prefix <> "image"+      BookmarkBlock {url} ->+        putStrLn $ prefix <> "bookmark: " <> Text.unpack url+      DividerBlock -> putStrLn $ prefix <> "divider"+      _ -> putStrLn $ prefix <> Text.unpack (Blocks.type_ block)++  -- -----------------------------------------------------------------------+  -- Section 6: Nested blocks — text-like parents+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "Nested Blocks — Text Parents")++  let textParentBlocks =+        Vector.fromList+          [ headingBlock 2 (mkRichText "Nested Block Examples"),+            -- 1. Toggle with paragraph children (most common use case)+            toggleBlock (mkRichText "Toggle with children")+              `withChildren` Vector.fromList+                [ textBlock "First child inside toggle.",+                  textBlock "Second child inside toggle."+                ],+            -- 2. Paragraph with children+            paragraphBlock (mkRichText "Paragraph with nested content")+              `withChildren` Vector.fromList+                [ textBlock "Child of paragraph."+                ],+            -- 3. Bulleted list with nested sub-items+            bulletedListItemBlock (mkRichText "Parent bullet")+              `withChildren` Vector.fromList+                [ bulletedListItemBlock (mkRichText "Sub-bullet A"),+                  bulletedListItemBlock (mkRichText "Sub-bullet B")+                ],+            -- 4. Numbered list with nested sub-items+            numberedListItemBlock (mkRichText "Parent numbered item")+              `withChildren` Vector.fromList+                [ numberedListItemBlock (mkRichText "Sub-item 1"),+                  numberedListItemBlock (mkRichText "Sub-item 2")+                ],+            -- 5. To-do with children+            toDoBlock (mkRichText "Task with sub-tasks") False+              `withChildren` Vector.fromList+                [ toDoBlock (mkRichText "Sub-task A") True,+                  toDoBlock (mkRichText "Sub-task B") False+                ],+            -- 6. Quote with children+            quoteBlock (mkRichText "Quote with attribution")+              `withChildren` Vector.fromList+                [ textBlock "— Source of the quote"+                ],+            -- 7. Callout with children+            calloutBlock (mkRichText "Important notice") (Just (EmojiIcon "⚠️"))+              `withChildren` Vector.fromList+                [ textBlock "Additional details inside the callout.",+                  bulletedListItemBlock (mkRichText "Action item one"),+                  bulletedListItemBlock (mkRichText "Action item two")+                ],+            dividerBlock+          ]+      appendReq5 = AppendBlockChildren {children = textParentBlocks, position = Nothing}++  result5 <-+    runTest (Text.pack "Appending text-parent nested blocks") $+      appendBlockChildren methods testPageId appendReq5+  let result5List = Vector.toList (results result5)+  putStrLn $ "Appended " <> show (length result5List) <> " blocks"++  -- Verify has_children on each parent block+  putStrLn "\nVerifying has_children on returned blocks:"+  let parentIndices = [1, 2, 3, 4, 5, 6, 7] :: [Int] -- indices of blocks with children (0 = heading)+  mapM_+    ( \i -> do+        let b = result5List !! i+        putStrLn $+          "  "+            <> Text.unpack (Blocks.type_ b)+            <> ": has_children="+            <> show (Blocks.hasChildren b)+    )+    parentIndices++  -- Fetch children of the toggle and verify count+  let toggleObj = result5List !! 1+  toggleKids <-+    runTest (Text.pack "Fetching toggle children") $+      listBlockChildren methods (Blocks.id toggleObj) Nothing Nothing+  putStrLn $ "Toggle child count: " <> show (Vector.length $ results toggleKids)++  -- Fetch children of the callout and verify types+  let calloutObj = result5List !! 7+  calloutKids <-+    runTest (Text.pack "Fetching callout children") $+      listBlockChildren methods (Blocks.id calloutObj) Nothing Nothing+  putStrLn "Callout children:"+  Vector.forM_ (results calloutKids) $ \kid ->+    putStrLn $ "  - " <> Text.unpack (Blocks.type_ kid)++  -- -----------------------------------------------------------------------+  -- Section 7: Nested blocks — toggleable headings+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "Nested Blocks — Toggleable Headings")++  let headingBlocks =+        Vector.fromList+          [ -- Toggleable heading 1 with children+            Heading1Block+              { richText = mkRichText "Toggleable H1 with content",+                color = Default,+                isToggleable = True,+                children =+                  Vector.fromList+                    [ textBlock "Content revealed when H1 is expanded.",+                      codeBlock (mkRichText "putStrLn \"inside heading\"") Haskell+                    ]+              },+            -- Toggleable heading 2+            Heading2Block+              { richText = mkRichText "Toggleable H2",+                color = Default,+                isToggleable = True,+                children = Vector.singleton (textBlock "H2 nested content.")+              },+            -- Toggleable heading 3+            Heading3Block+              { richText = mkRichText "Toggleable H3",+                color = Default,+                isToggleable = True,+                children = Vector.singleton (textBlock "H3 nested content.")+              },+            -- Non-toggleable heading (no children) for comparison+            headingBlock 2 (mkRichText "Normal H2 (not toggleable, no children)"),+            dividerBlock+          ]+      appendReq6 = AppendBlockChildren {children = headingBlocks, position = Nothing}++  result6 <-+    runTest (Text.pack "Appending toggleable headings") $+      appendBlockChildren methods testPageId appendReq6+  let result6List = Vector.toList (results result6)+  putStrLn "Toggleable heading results:"+  mapM_+    ( \i -> do+        let b = result6List !! i+        putStrLn $+          "  "+            <> Text.unpack (Blocks.type_ b)+            <> ": has_children="+            <> show (Blocks.hasChildren b)+    )+    [0, 1, 2, 3 :: Int]++  -- Fetch children of togglable H1+  let h1Obj = result6List !! 0+  h1Kids <-+    runTest (Text.pack "Fetching toggleable H1 children") $+      listBlockChildren methods (Blocks.id h1Obj) Nothing Nothing+  putStrLn $ "H1 child count: " <> show (Vector.length $ results h1Kids)+  Vector.forM_ (results h1Kids) $ \kid ->+    putStrLn $ "  - " <> Text.unpack (Blocks.type_ kid)++  -- -----------------------------------------------------------------------+  -- Section 8: Nested blocks — structural (columns, tables)+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "Nested Blocks — Columns and Tables")++  let structuralBlocks =+        Vector.fromList+          [ headingBlock 2 (mkRichText "Column Layout"),+            -- Column list with 3 columns, each containing multiple blocks+            ColumnListBlock+              { children =+                  Vector.fromList+                    [ ColumnBlock+                        { children =+                            Vector.fromList+                              [ headingBlock 3 (mkRichText "Column A"),+                                textBlock "Content in column A."+                              ]+                        },+                      ColumnBlock+                        { children =+                            Vector.fromList+                              [ headingBlock 3 (mkRichText "Column B"),+                                textBlock "Content in column B."+                              ]+                        },+                      ColumnBlock+                        { children =+                            Vector.fromList+                              [ headingBlock 3 (mkRichText "Column C"),+                                textBlock "Content in column C."+                              ]+                        }+                    ]+              },+            headingBlock 2 (mkRichText "Table with Row Children"),+            -- Table with inline table_row children+            TableBlock+              { tableWidth = 3,+                hasColumnHeader = True,+                hasRowHeader = False,+                children =+                  Vector.fromList+                    [ TableRowBlock+                        { cells =+                            Vector.fromList+                              [ mkRichText "Name",+                                mkRichText "Language",+                                mkRichText "Year"+                              ]+                        },+                      TableRowBlock+                        { cells =+                            Vector.fromList+                              [ mkRichText "GHC",+                                mkRichText "Haskell",+                                mkRichText "1992"+                              ]+                        },+                      TableRowBlock+                        { cells =+                            Vector.fromList+                              [ mkRichText "Cabal",+                                mkRichText "Haskell",+                                mkRichText "2004"+                              ]+                        }+                    ]+              },+            dividerBlock+          ]+      appendReq7 = AppendBlockChildren {children = structuralBlocks, position = Nothing}++  result7 <-+    runTest (Text.pack "Appending columns and table") $+      appendBlockChildren methods testPageId appendReq7+  let result7List = Vector.toList (results result7)++  -- Verify column_list has children+  let columnListObj = result7List !! 1+  putStrLn $ "column_list has_children: " <> show (Blocks.hasChildren columnListObj)++  -- Fetch columns+  columnKids <-+    runTest (Text.pack "Fetching column_list children") $+      listBlockChildren methods (Blocks.id columnListObj) Nothing Nothing+  putStrLn $ "Number of columns: " <> show (Vector.length $ results columnKids)++  -- Fetch content of first column+  let firstCol = Vector.head (results columnKids)+  colContent <-+    runTest (Text.pack "Fetching first column content") $+      listBlockChildren methods (Blocks.id firstCol) Nothing Nothing+  putStrLn "First column content:"+  Vector.forM_ (results colContent) $ \kid ->+    putStrLn $ "  - " <> Text.unpack (Blocks.type_ kid)++  -- Verify table has children (rows)+  let tableObj = result7List !! 3+  putStrLn $ "\ntable has_children: " <> show (Blocks.hasChildren tableObj)++  tableKids <-+    runTest (Text.pack "Fetching table rows") $+      listBlockChildren methods (Blocks.id tableObj) Nothing Nothing+  putStrLn $ "Table row count: " <> show (Vector.length $ results tableKids)++  -- -----------------------------------------------------------------------+  -- Section 9: Nested blocks — synced block and two-level nesting+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "Nested Blocks — Synced Block & Two-Level Nesting")++  let advancedBlocks =+        Vector.fromList+          [ headingBlock 2 (mkRichText "Synced Block with Children"),+            -- Original synced block with children+            SyncedBlockContent+              { syncedFrom = SyncedOriginal,+                children =+                  Vector.fromList+                    [ textBlock "Content inside the synced block.",+                      bulletedListItemBlock (mkRichText "Synced list item")+                    ]+              },+            headingBlock 2 (mkRichText "Two-Level Nesting"),+            -- Toggle containing a quote containing a paragraph (2 levels deep)+            toggleBlock (mkRichText "Outer toggle (level 1)")+              `withChildren` Vector.fromList+                [ quoteBlock (mkRichText "Inner quote (level 2)")+                    `withChildren` Vector.fromList+                      [ textBlock "Deepest content (grandchild)."+                      ],+                  textBlock "Also inside the toggle."+                ],+            -- Bulleted list with nested numbered sub-items+            bulletedListItemBlock (mkRichText "Mixed nesting: bullet parent")+              `withChildren` Vector.fromList+                [ numberedListItemBlock (mkRichText "Numbered sub-item 1"),+                  numberedListItemBlock (mkRichText "Numbered sub-item 2"),+                  toDoBlock (mkRichText "To-do sub-item") False+                ],+            dividerBlock+          ]+      appendReq8 = AppendBlockChildren {children = advancedBlocks, position = Nothing}++  result8 <-+    runTest (Text.pack "Appending synced block and two-level nesting") $+      appendBlockChildren methods testPageId appendReq8+  let result8List = Vector.toList (results result8)++  -- Verify synced block+  let syncedObj = result8List !! 1+  putStrLn $ "synced_block has_children: " <> show (Blocks.hasChildren syncedObj)+  syncedKids <-+    runTest (Text.pack "Fetching synced_block children") $+      listBlockChildren methods (Blocks.id syncedObj) Nothing Nothing+  putStrLn $ "Synced block child count: " <> show (Vector.length $ results syncedKids)+  Vector.forM_ (results syncedKids) $ \kid ->+    putStrLn $ "  - " <> Text.unpack (Blocks.type_ kid)++  -- Verify two-level nesting: outer toggle → inner quote → grandchild paragraph+  let outerToggle = result8List !! 3+  putStrLn $ "\nOuter toggle has_children: " <> show (Blocks.hasChildren outerToggle)+  outerKids <-+    runTest (Text.pack "Fetching outer toggle children") $+      listBlockChildren methods (Blocks.id outerToggle) Nothing Nothing+  putStrLn $ "Outer toggle child count: " <> show (Vector.length $ results outerKids)++  -- The first child should be the quote, which itself has children+  let innerQuote = Vector.head (results outerKids)+  putStrLn $ "Inner quote has_children: " <> show (Blocks.hasChildren innerQuote)+  grandKids <-+    runTest (Text.pack "Fetching grandchildren (quote's children)") $+      listBlockChildren methods (Blocks.id innerQuote) Nothing Nothing+  putStrLn $ "Grandchild count: " <> show (Vector.length $ results grandKids)+  Vector.forM_ (results grandKids) $ \kid ->+    putStrLn $ "  - " <> Text.unpack (Blocks.type_ kid)++  -- Verify mixed nesting bullet+  let mixedBullet = result8List !! 4+  mixedKids <-+    runTest (Text.pack "Fetching mixed-nesting bullet children") $+      listBlockChildren methods (Blocks.id mixedBullet) Nothing Nothing+  putStrLn $ "\nMixed bullet child count: " <> show (Vector.length $ results mixedKids)+  Vector.forM_ (results mixedKids) $ \kid ->+    putStrLn $ "  - " <> Text.unpack (Blocks.type_ kid)++  -- -----------------------------------------------------------------------+  -- Section 10: withChildren combinator showcase+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "withChildren Combinator Showcase")++  let combinatorBlocks =+        Vector.fromList+          [ headingBlock 2 (mkRichText "withChildren Combinator"),+            -- withChildren on various block types+            paragraphBlock (mkRichText "Paragraph via withChildren")+              `withChildren` Vector.singleton (textBlock "Nested under paragraph."),+            calloutBlock (mkRichText "Callout via withChildren") (Just (EmojiIcon "📌"))+              `withChildren` Vector.fromList+                [ textBlock "Step 1: Read the docs.",+                  textBlock "Step 2: Write the code.",+                  textBlock "Step 3: Ship it."+                ],+            -- withChildren on a block that doesn't support children (no-op)+            -- codeBlock and dividerBlock are unchanged by withChildren+            codeBlock (mkRichText "-- withChildren is a no-op on code blocks") Haskell+              `withChildren` Vector.singleton (textBlock "This child is silently dropped."),+            dividerBlock+          ]+      appendReq9 = AppendBlockChildren {children = combinatorBlocks, position = Nothing}++  result9 <-+    runTest (Text.pack "Appending withChildren showcase") $+      appendBlockChildren methods testPageId appendReq9+  let result9List = Vector.toList (results result9)++  putStrLn "withChildren results:"+  putStrLn $ "  paragraph has_children: " <> show (Blocks.hasChildren (result9List !! 1))+  putStrLn $ "  callout has_children: " <> show (Blocks.hasChildren (result9List !! 2))+  putStrLn $ "  code has_children: " <> show (Blocks.hasChildren (result9List !! 3)) <> " (expected False)"++  -- -----------------------------------------------------------------------+  -- Section 11: Position-based insertion+  -- -----------------------------------------------------------------------+  printHeader (Text.pack "Position-based Insertion")++  -- Insert at the start of the page+  let startBlock = textBlock "This block was inserted at the START of the page."+      startReq = AppendBlockChildren {children = Vector.singleton startBlock, position = Just Start}+  _ <-+    runTest (Text.pack "Inserting block at start") $+      appendBlockChildren methods testPageId startReq+  putStrLn "Block inserted at start position"++  -- Verify the first block is our new paragraph+  refreshed <-+    runTest (Text.pack "Verifying insertion order") $+      listBlockChildren methods testPageId (Just 1) Nothing+  let List {results = firstResults} = refreshed+  case Vector.toList firstResults of+    (first : _) ->+      putStrLn $ "First block type: " <> Text.unpack (Blocks.type_ first)+    [] ->+      putStrLn "No blocks found (unexpected)"++  -- Clean up: move page to trash+  printHeader (Text.pack "Cleanup")+  _ <-+    runTest (Text.pack "Trashing test page") $+      deleteBlock methods testPageId+  putStrLn "Test page moved to trash"+  putStrLn "Block demo complete!"
notion-client-example/Blocks.hs view
@@ -1,7 +1,8 @@ -- |--- Helper functions for creating Notion block JSON objects.+-- Helper functions for creating Notion block objects. ----- These functions create JSON objects that match the Notion API's block format.+-- These functions create typed block objects using the smart constructors+-- from "Notion.V1.BlockContent". module Blocks   ( createParagraphBlock,     createHeadingBlock,@@ -9,59 +10,17 @@   ) where -import Data.Aeson qualified as Aeson-import Data.String (fromString) import Data.Text (Text)-import Data.Text qualified as Text-import Data.Vector qualified as Vector+import Notion.V1.BlockContent  -- | Create a paragraph block with text content--- @param content The text content to include in the paragraph--- @return A JSON representation of a paragraph block-createParagraphBlock :: Text -> Aeson.Value-createParagraphBlock content =-  let -- Structure: {"text": {"content": "Your text here"}}-      textObj = Aeson.object [("content", Aeson.String content)]-      textItem = Aeson.object [("text", textObj)]-      -- Rich text is always an array of text items-      richText = Aeson.Array (Vector.singleton textItem)-      -- The paragraph property contains rich_text array-      paragraphContent = Aeson.object [("rich_text", richText)]-   in Aeson.object-        [ ("type", Aeson.String "paragraph"), -- Block type-          ("paragraph", paragraphContent) -- Block content-        ]+createParagraphBlock :: Text -> BlockContent+createParagraphBlock = textBlock  -- | Create a heading block with text content--- @param content The text content for the heading--- @param level The heading level (1-3)--- @return A JSON representation of a heading block-createHeadingBlock :: Text -> Int -> Aeson.Value-createHeadingBlock content level =-  let -- Create the same rich text structure as paragraph-      textObj = Aeson.object [("content", Aeson.String content)]-      textItem = Aeson.object [("text", textObj)]-      richText = Aeson.Array (Vector.singleton textItem)-      -- Headings use heading_1, heading_2, or heading_3 as type-      headingType = "heading_" <> Text.pack (show level)-      headingContent = Aeson.object [("rich_text", richText)]-   in Aeson.object-        [ ("type", Aeson.String headingType), -- Block type-          (fromString (Text.unpack headingType), headingContent) -- Block content-        ]+createHeadingBlock :: Text -> Int -> BlockContent+createHeadingBlock t level = headingBlock level (mkRichText t)  -- | Create a bulleted list item block with text content--- @param content The text content for the list item--- @return A JSON representation of a bulleted list item block-createBulletedListItemBlock :: Text -> Aeson.Value-createBulletedListItemBlock content =-  let -- Create the same rich text structure as other blocks-      textObj = Aeson.object [("content", Aeson.String content)]-      textItem = Aeson.object [("text", textObj)]-      richText = Aeson.Array (Vector.singleton textItem)-      -- List items are structured the same way as paragraphs-      listContent = Aeson.object [("rich_text", richText)]-   in Aeson.object-        [ ("type", Aeson.String "bulleted_list_item"), -- Block type-          ("bulleted_list_item", listContent) -- Block content-        ]+createBulletedListItemBlock :: Text -> BlockContent+createBulletedListItemBlock = bulletedListItemBlock . mkRichText
notion-client-example/Main.hs view
@@ -28,6 +28,7 @@ -- Note: Your integration must have access to the specified database and page. module Main where +import BlockDemo (runBlockDemo) import Console (logError, printHeader, runTest) import Control.Monad (when) import CustomEmojiDemo (runCustomEmojiDemo)@@ -79,8 +80,9 @@     Just pageIdStr -> do       runPageDemo methods pageIdStr       runMarkdownDemo methods pageIdStr+      runBlockDemo methods pageIdStr     Nothing ->-      putStrLn "Skipping page/markdown tests (set NOTION_TEST_PAGE_ID to enable)"+      putStrLn "Skipping page/markdown/block tests (set NOTION_TEST_PAGE_ID to enable)"    -- Optional Database tests   case databaseIdEnv of
notion-client-example/PageDemo.hs view
@@ -7,14 +7,14 @@  import Console (printHeader, runTest) import Control.Monad (when)-import Data.Aeson qualified as Aeson import Data.String (fromString) import Data.Text qualified as Text import Data.Vector qualified as Vector import Notion.V1 (Methods (..))+import Notion.V1.BlockContent (BlockContent, CodeLanguage (..), calloutBlock, codeBlock, mkRichText, quoteBlock) import Notion.V1.Blocks qualified as Blocks import Notion.V1.Comments (CommentObject (..), CreateComment (..))-import Notion.V1.Common (Parent (..))+import Notion.V1.Common (Icon (..), Parent (..)) import Notion.V1.ListOf (ListOf (..)) import Notion.V1.RichText (RichText (..), RichTextContent (..), TextContent (..), defaultAnnotations) import Prelude hiding (id)@@ -93,57 +93,17 @@     putStrLn $ "Block has " <> show (Vector.length blockCommentResults) <> " comments"    -- Add new blocks to the existing page-  let codeBlock =-        Aeson.object-          [ ("type", Aeson.String "code"),-            ( "code",-              Aeson.object-                [ ( "rich_text",-                    Aeson.Array-                      ( Vector.singleton $-                          Aeson.object-                            [ ("text", Aeson.object [("content", Aeson.String "const example = () => {\n  console.log('Hello from Notion API');\n};")])-                            ]-                      )-                  ),-                  ("language", Aeson.String "javascript")-                ]-            )-          ]-      quoteBlock =-        Aeson.object-          [ ("type", Aeson.String "quote"),-            ( "quote",-              Aeson.object-                [ ( "rich_text",-                    Aeson.Array-                      ( Vector.singleton $-                          Aeson.object-                            [ ("text", Aeson.object [("content", Aeson.String "This is a quote block added via the API")])-                            ]-                      )-                  )-                ]-            )-          ]-      calloutBlock =-        Aeson.object-          [ ("type", Aeson.String "callout"),-            ( "callout",-              Aeson.object-                [ ( "rich_text",-                    Aeson.Array-                      ( Vector.singleton $-                          Aeson.object-                            [ ("text", Aeson.object [("content", Aeson.String "This is a callout block with an emoji")])-                            ]-                      )-                  ),-                  ("icon", Aeson.object [("emoji", Aeson.String "🔥")])-                ]-            )-          ]-      specializedBlocks = Vector.fromList [codeBlock, quoteBlock, calloutBlock]+  let codeBlockVal =+        codeBlock+          (mkRichText "const example = () => {\n  console.log('Hello from Notion API');\n};")+          JavaScript+      quoteBlockVal =+        quoteBlock (mkRichText "This is a quote block added via the API")+      calloutBlockVal =+        calloutBlock+          (mkRichText "This is a callout block with an emoji")+          (Just (EmojiIcon "🔥"))+      specializedBlocks = Vector.fromList [codeBlockVal, quoteBlockVal, calloutBlockVal]       appendRequest = Blocks.AppendBlockChildren {children = specializedBlocks, position = Nothing}    -- Append blocks to the existing page
notion-client.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.4 name:               notion-client-version:            0.5.0.0+version:            0.6.0.0 synopsis:           Type-safe Haskell client for the Notion API description:   This package provides comprehensive and type-safe bindings@@ -57,6 +57,7 @@    exposed-modules:     Notion.V1+    Notion.V1.BlockContent     Notion.V1.Blocks     Notion.V1.Comments     Notion.V1.Common@@ -118,6 +119,7 @@   hs-source-dirs:     notion-client-example   main-is:            Main.hs   other-modules:+    BlockDemo     Blocks     Console     CustomEmojiDemo
src/Notion/V1.hs view
@@ -203,7 +203,7 @@       IO PageObject,     -- \* Blocks     retrieveBlock :: BlockID -> IO BlockObject,-    updateBlock :: BlockID -> Blocks.BlockContent -> IO BlockObject,+    updateBlock :: BlockID -> Blocks.BlockUpdate -> IO BlockObject,     listBlockChildren ::       ParentID ->       Maybe Natural ->
+ src/Notion/V1/BlockContent.hs view
@@ -0,0 +1,988 @@+-- | Typed block content for Notion API blocks.+--+-- Each Notion block carries a @type@ discriminator (e.g., @\"paragraph\"@,+-- @\"heading_1\"@, @\"code\"@) and the actual content nested under a key+-- matching that type name. This module replaces the untyped @Value@ with a+-- proper Haskell sum type so consumers can pattern-match on block types and+-- use smart constructors to build well-formed blocks.+module Notion.V1.BlockContent+  ( -- * Block content+    BlockContent (..),+    blockContentType,+    blockContentFields,+    parseBlockContent,++    -- * Block update wrapper+    BlockUpdate (..),++    -- * Supporting types+    CodeLanguage (..),+    FileSource (..),+    ListFormat (..),+    SyncedFrom (..),+    LinkTarget (..),++    -- * Smart constructors+    mkRichText,+    textBlock,+    paragraphBlock,+    headingBlock,+    bulletedListItemBlock,+    numberedListItemBlock,+    toDoBlock,+    toggleBlock,+    quoteBlock,+    calloutBlock,+    codeBlock,+    equationBlock,+    bookmarkBlock,+    dividerBlock,+    imageBlock,++    -- * Combinators+    withChildren,+  )+where++import Data.Aeson (object, (.:), (.:?), (.=))+import Data.Aeson qualified as Aeson+import Data.Aeson.Key qualified as Key+import Data.Aeson.Types (Pair, Parser)+import Data.Maybe (fromMaybe)+import Data.Vector qualified as Vector+import Notion.Prelude+import Notion.V1.Common (Color (..), ExternalFile, File, Icon, UUID)+import Notion.V1.RichText (RichText (..), RichTextContent (..), TextContent (..), defaultAnnotations)++-- ---------------------------------------------------------------------------+-- Supporting types+-- ---------------------------------------------------------------------------++-- | Programming language for code blocks.+data CodeLanguage+  = Abap+  | Arduino+  | Bash+  | Basic+  | C+  | Clojure+  | CoffeeScript+  | Cpp+  | CSharp+  | Css+  | Dart+  | Diff+  | Docker+  | Elixir+  | Elm+  | Erlang+  | Flow+  | Fortran+  | FSharp+  | Gherkin+  | Glsl+  | Go+  | GraphQL+  | Groovy+  | Haskell+  | Html+  | Java+  | JavaScript+  | Json+  | Julia+  | Kotlin+  | LaTeX+  | Less+  | Lisp+  | LiveScript+  | Lua+  | Makefile+  | Markdown+  | Markup+  | Matlab+  | Mermaid+  | Nix+  | ObjectiveC+  | OCaml+  | Pascal+  | Perl+  | Php+  | PlainText+  | PowerShell+  | Prolog+  | Protobuf+  | Python+  | R+  | Reason+  | Ruby+  | Rust+  | Sass+  | Scala+  | Scheme+  | Scss+  | Shell+  | Sql+  | Swift+  | TypeScript+  | VbNet+  | Verilog+  | Vhdl+  | VisualBasic+  | WebAssembly+  | Xml+  | Yaml+  | JavaCCppCSharp+  deriving stock (Eq, Show, Generic)++instance FromJSON CodeLanguage where+  parseJSON = Aeson.withText "CodeLanguage" $ \case+    "abap" -> pure Abap+    "arduino" -> pure Arduino+    "bash" -> pure Bash+    "basic" -> pure Basic+    "c" -> pure C+    "clojure" -> pure Clojure+    "coffeescript" -> pure CoffeeScript+    "c++" -> pure Cpp+    "c#" -> pure CSharp+    "css" -> pure Css+    "dart" -> pure Dart+    "diff" -> pure Diff+    "docker" -> pure Docker+    "elixir" -> pure Elixir+    "elm" -> pure Elm+    "erlang" -> pure Erlang+    "flow" -> pure Flow+    "fortran" -> pure Fortran+    "f#" -> pure FSharp+    "gherkin" -> pure Gherkin+    "glsl" -> pure Glsl+    "go" -> pure Go+    "graphql" -> pure GraphQL+    "groovy" -> pure Groovy+    "haskell" -> pure Haskell+    "html" -> pure Html+    "java" -> pure Java+    "javascript" -> pure JavaScript+    "json" -> pure Json+    "julia" -> pure Julia+    "kotlin" -> pure Kotlin+    "latex" -> pure LaTeX+    "less" -> pure Less+    "lisp" -> pure Lisp+    "livescript" -> pure LiveScript+    "lua" -> pure Lua+    "makefile" -> pure Makefile+    "markdown" -> pure Markdown+    "markup" -> pure Markup+    "matlab" -> pure Matlab+    "mermaid" -> pure Mermaid+    "nix" -> pure Nix+    "objective-c" -> pure ObjectiveC+    "ocaml" -> pure OCaml+    "pascal" -> pure Pascal+    "perl" -> pure Perl+    "php" -> pure Php+    "plain text" -> pure PlainText+    "powershell" -> pure PowerShell+    "prolog" -> pure Prolog+    "protobuf" -> pure Protobuf+    "python" -> pure Python+    "r" -> pure R+    "reason" -> pure Reason+    "ruby" -> pure Ruby+    "rust" -> pure Rust+    "sass" -> pure Sass+    "scala" -> pure Scala+    "scheme" -> pure Scheme+    "scss" -> pure Scss+    "shell" -> pure Shell+    "sql" -> pure Sql+    "swift" -> pure Swift+    "typescript" -> pure TypeScript+    "vb.net" -> pure VbNet+    "verilog" -> pure Verilog+    "vhdl" -> pure Vhdl+    "visual basic" -> pure VisualBasic+    "webassembly" -> pure WebAssembly+    "xml" -> pure Xml+    "yaml" -> pure Yaml+    "java/c/c++/c#" -> pure JavaCCppCSharp+    other -> fail $ "Unknown CodeLanguage: " <> unpack other++instance ToJSON CodeLanguage where+  toJSON = \case+    Abap -> Aeson.String "abap"+    Arduino -> Aeson.String "arduino"+    Bash -> Aeson.String "bash"+    Basic -> Aeson.String "basic"+    C -> Aeson.String "c"+    Clojure -> Aeson.String "clojure"+    CoffeeScript -> Aeson.String "coffeescript"+    Cpp -> Aeson.String "c++"+    CSharp -> Aeson.String "c#"+    Css -> Aeson.String "css"+    Dart -> Aeson.String "dart"+    Diff -> Aeson.String "diff"+    Docker -> Aeson.String "docker"+    Elixir -> Aeson.String "elixir"+    Elm -> Aeson.String "elm"+    Erlang -> Aeson.String "erlang"+    Flow -> Aeson.String "flow"+    Fortran -> Aeson.String "fortran"+    FSharp -> Aeson.String "f#"+    Gherkin -> Aeson.String "gherkin"+    Glsl -> Aeson.String "glsl"+    Go -> Aeson.String "go"+    GraphQL -> Aeson.String "graphql"+    Groovy -> Aeson.String "groovy"+    Haskell -> Aeson.String "haskell"+    Html -> Aeson.String "html"+    Java -> Aeson.String "java"+    JavaScript -> Aeson.String "javascript"+    Json -> Aeson.String "json"+    Julia -> Aeson.String "julia"+    Kotlin -> Aeson.String "kotlin"+    LaTeX -> Aeson.String "latex"+    Less -> Aeson.String "less"+    Lisp -> Aeson.String "lisp"+    LiveScript -> Aeson.String "livescript"+    Lua -> Aeson.String "lua"+    Makefile -> Aeson.String "makefile"+    Markdown -> Aeson.String "markdown"+    Markup -> Aeson.String "markup"+    Matlab -> Aeson.String "matlab"+    Mermaid -> Aeson.String "mermaid"+    Nix -> Aeson.String "nix"+    ObjectiveC -> Aeson.String "objective-c"+    OCaml -> Aeson.String "ocaml"+    Pascal -> Aeson.String "pascal"+    Perl -> Aeson.String "perl"+    Php -> Aeson.String "php"+    PlainText -> Aeson.String "plain text"+    PowerShell -> Aeson.String "powershell"+    Prolog -> Aeson.String "prolog"+    Protobuf -> Aeson.String "protobuf"+    Python -> Aeson.String "python"+    R -> Aeson.String "r"+    Reason -> Aeson.String "reason"+    Ruby -> Aeson.String "ruby"+    Rust -> Aeson.String "rust"+    Sass -> Aeson.String "sass"+    Scala -> Aeson.String "scala"+    Scheme -> Aeson.String "scheme"+    Scss -> Aeson.String "scss"+    Shell -> Aeson.String "shell"+    Sql -> Aeson.String "sql"+    Swift -> Aeson.String "swift"+    TypeScript -> Aeson.String "typescript"+    VbNet -> Aeson.String "vb.net"+    Verilog -> Aeson.String "verilog"+    Vhdl -> Aeson.String "vhdl"+    VisualBasic -> Aeson.String "visual basic"+    WebAssembly -> Aeson.String "webassembly"+    Xml -> Aeson.String "xml"+    Yaml -> Aeson.String "yaml"+    JavaCCppCSharp -> Aeson.String "java/c/c++/c#"++-- | File source for media blocks (image, video, audio, file, pdf).+--+-- The API uses a @type@ discriminator with values @\"external\"@, @\"file\"@,+-- or @\"file_upload\"@, and the content nested under the corresponding key.+data FileSource+  = ExternalSource ExternalFile+  | NotionSource File+  | FileUploadSource UUID+  deriving stock (Eq, Show)++parseFileSource :: Aeson.Object -> Parser FileSource+parseFileSource o = do+  srcType <- o .: "type"+  case srcType of+    "external" -> ExternalSource <$> o .: "external"+    "file" -> NotionSource <$> o .: "file"+    "file_upload" -> do+      uploadObj <- o .: "file_upload"+      FileUploadSource <$> uploadObj .: "id"+    other -> fail $ "Unknown file source type: " <> unpack (other :: Text)++instance FromJSON FileSource where+  parseJSON = \case+    Object o -> parseFileSource o+    _ -> fail "Expected object for FileSource"++instance ToJSON FileSource where+  toJSON = object . fileSourcePairs++fileSourcePairs :: FileSource -> [Pair]+fileSourcePairs = \case+  ExternalSource ef -> ["type" .= ("external" :: Text), "external" .= ef]+  NotionSource f -> ["type" .= ("file" :: Text), "file" .= f]+  FileUploadSource uid -> ["type" .= ("file_upload" :: Text), "file_upload" .= object ["id" .= uid]]++-- | List format for numbered list items.+data ListFormat+  = Numbers+  | Letters+  | Roman+  deriving stock (Eq, Show, Generic)++instance FromJSON ListFormat where+  parseJSON = Aeson.withText "ListFormat" $ \case+    "numbers" -> pure Numbers+    "letters" -> pure Letters+    "roman" -> pure Roman+    other -> fail $ "Unknown ListFormat: " <> unpack other++instance ToJSON ListFormat where+  toJSON = \case+    Numbers -> Aeson.String "numbers"+    Letters -> Aeson.String "letters"+    Roman -> Aeson.String "roman"++-- | Synced block origin. An original synced block has @synced_from: null@ in+-- the API; a reference points to the original block by ID.+data SyncedFrom+  = SyncedOriginal+  | SyncedReference UUID+  deriving stock (Eq, Show)++instance FromJSON SyncedFrom where+  parseJSON = \case+    Null -> pure SyncedOriginal+    Object o -> SyncedReference <$> o .: "block_id"+    _ -> fail "Expected null or object for SyncedFrom"++instance ToJSON SyncedFrom where+  toJSON SyncedOriginal = Null+  toJSON (SyncedReference bid) =+    object ["type" .= ("block_id" :: Text), "block_id" .= bid]++-- | Target of a @link_to_page@ block.+data LinkTarget+  = LinkToPage UUID+  | LinkToDatabase UUID+  | LinkToComment UUID+  deriving stock (Eq, Show)++instance FromJSON LinkTarget where+  parseJSON = \case+    Object o -> do+      t <- o .: "type"+      case t of+        "page_id" -> LinkToPage <$> o .: "page_id"+        "database_id" -> LinkToDatabase <$> o .: "database_id"+        "comment_id" -> LinkToComment <$> o .: "comment_id"+        other -> fail $ "Unknown LinkTarget type: " <> unpack (other :: Text)+    _ -> fail "Expected object for LinkTarget"++instance ToJSON LinkTarget where+  toJSON (LinkToPage pid) = object ["type" .= ("page_id" :: Text), "page_id" .= pid]+  toJSON (LinkToDatabase did) = object ["type" .= ("database_id" :: Text), "database_id" .= did]+  toJSON (LinkToComment cid) = object ["type" .= ("comment_id" :: Text), "comment_id" .= cid]++-- ---------------------------------------------------------------------------+-- BlockContent+-- ---------------------------------------------------------------------------++-- | Typed block content. Each constructor corresponds to one Notion block type.+--+-- Use the smart constructors ('paragraphBlock', 'headingBlock', 'codeBlock',+-- etc.) to build blocks conveniently, or construct values directly for full+-- control over all fields.+data BlockContent+  = -- | Paragraph block with rich text, color, and optional icon.+    ParagraphBlock+      { richText :: Vector RichText,+        color :: Color,+        paragraphIcon :: Maybe Icon,+        children :: Vector BlockContent+      }+  | -- | Heading level 1.+    --+    -- Children are only accepted by the API when @isToggleable@ is @True@.+    Heading1Block+      { richText :: Vector RichText,+        color :: Color,+        isToggleable :: Bool,+        children :: Vector BlockContent+      }+  | -- | Heading level 2.+    --+    -- Children are only accepted by the API when @isToggleable@ is @True@.+    Heading2Block+      { richText :: Vector RichText,+        color :: Color,+        isToggleable :: Bool,+        children :: Vector BlockContent+      }+  | -- | Heading level 3.+    --+    -- Children are only accepted by the API when @isToggleable@ is @True@.+    Heading3Block+      { richText :: Vector RichText,+        color :: Color,+        isToggleable :: Bool,+        children :: Vector BlockContent+      }+  | -- | Bulleted list item.+    BulletedListItemBlock+      { richText :: Vector RichText,+        color :: Color,+        children :: Vector BlockContent+      }+  | -- | Numbered list item with optional format and start index.+    NumberedListItemBlock+      { richText :: Vector RichText,+        color :: Color,+        listFormat :: Maybe ListFormat,+        listStartIndex :: Maybe Natural,+        children :: Vector BlockContent+      }+  | -- | To-do checkbox item.+    ToDoBlock+      { richText :: Vector RichText,+        color :: Color,+        checked :: Bool,+        children :: Vector BlockContent+      }+  | -- | Toggle block (content revealed on click).+    ToggleBlock+      { richText :: Vector RichText,+        color :: Color,+        children :: Vector BlockContent+      }+  | -- | Quote block.+    QuoteBlock+      { richText :: Vector RichText,+        color :: Color,+        children :: Vector BlockContent+      }+  | -- | Callout block with icon.+    CalloutBlock+      { richText :: Vector RichText,+        color :: Color,+        calloutIcon :: Maybe Icon,+        children :: Vector BlockContent+      }+  | -- | Code block with language.+    CodeBlock+      { richText :: Vector RichText,+        caption :: Vector RichText,+        language :: CodeLanguage+      }+  | -- | KaTeX equation block.+    EquationBlock+      { expression :: Text+      }+  | -- | Image block.+    ImageBlock+      { imageSource :: FileSource,+        caption :: Vector RichText+      }+  | -- | Video block.+    VideoBlock+      { videoSource :: FileSource,+        caption :: Vector RichText+      }+  | -- | Audio block.+    AudioBlock+      { audioSource :: FileSource+      }+  | -- | File attachment block.+    FileBlock+      { fileSource :: FileSource,+        caption :: Vector RichText,+        fileName :: Maybe Text+      }+  | -- | PDF block.+    PdfBlock+      { pdfSource :: FileSource,+        caption :: Vector RichText+      }+  | -- | Bookmark block.+    BookmarkBlock+      { url :: Text,+        caption :: Vector RichText+      }+  | -- | Embed block.+    EmbedBlock+      { url :: Text+      }+  | -- | Link to another page, database, or comment.+    LinkToPageBlock+      { linkTarget :: LinkTarget+      }+  | -- | Link preview (read-only).+    LinkPreviewBlock+      { url :: Text+      }+  | -- | Horizontal divider.+    DividerBlock+  | -- | Breadcrumb navigation.+    BreadcrumbBlock+  | -- | Table of contents.+    TableOfContentsBlock+      { color :: Color+      }+  | -- | Column list (container for columns).+    --+    -- Children must be 'ColumnBlock' values, with at least 2 entries.+    ColumnListBlock+      { children :: Vector BlockContent+      }+  | -- | Single column within a column list.+    ColumnBlock+      { children :: Vector BlockContent+      }+  | -- | Table block.+    --+    -- Children must be 'TableRowBlock' values.+    TableBlock+      { tableWidth :: Natural,+        hasColumnHeader :: Bool,+        hasRowHeader :: Bool,+        children :: Vector BlockContent+      }+  | -- | Table row.+    TableRowBlock+      { cells :: Vector (Vector RichText)+      }+  | -- | Child page reference (read-only, created via the Pages endpoint).+    ChildPageBlock+      { title :: Text+      }+  | -- | Child database reference (read-only, created via the Databases endpoint).+    ChildDatabaseBlock+      { title :: Text+      }+  | -- | Synced block (original or reference).+    --+    -- Children are only valid when @syncedFrom@ is 'SyncedOriginal'.+    SyncedBlockContent+      { syncedFrom :: SyncedFrom,+        children :: Vector BlockContent+      }+  | -- | Unsupported block type returned by the API.+    UnsupportedBlock+  | -- | Fallback for block types not yet modeled.+    UnknownBlock Text Value+  deriving stock (Eq, Generic, Show)++-- ---------------------------------------------------------------------------+-- Serialization helpers+-- ---------------------------------------------------------------------------++-- | Include @\"children\"@ key only when the vector is non-empty.+childrenPairs :: Vector BlockContent -> [Pair]+childrenPairs cs+  | Vector.null cs = []+  | otherwise = ["children" .= cs]++-- | Extract the JSON type name from a 'BlockContent' value.+blockContentType :: BlockContent -> Text+blockContentType = fst . blockContentFields++-- | Decompose a 'BlockContent' into its JSON type name and inner content+-- value. This is the serialization primitive used by both 'ToJSON BlockContent'+-- (full format with @\"type\"@ key) and 'ToJSON BlockUpdate' (update format+-- without @\"type\"@ key).+blockContentFields :: BlockContent -> (Text, Value)+blockContentFields = \case+  ParagraphBlock {..} ->+    ( "paragraph",+      object $+        ["rich_text" .= richText, "color" .= color]+          <> maybe [] (\i -> ["icon" .= i]) paragraphIcon+          <> childrenPairs children+    )+  Heading1Block {..} ->+    ( "heading_1",+      object $+        ["rich_text" .= richText, "color" .= color, "is_toggleable" .= isToggleable]+          <> childrenPairs children+    )+  Heading2Block {..} ->+    ( "heading_2",+      object $+        ["rich_text" .= richText, "color" .= color, "is_toggleable" .= isToggleable]+          <> childrenPairs children+    )+  Heading3Block {..} ->+    ( "heading_3",+      object $+        ["rich_text" .= richText, "color" .= color, "is_toggleable" .= isToggleable]+          <> childrenPairs children+    )+  BulletedListItemBlock {..} ->+    ( "bulleted_list_item",+      object $+        ["rich_text" .= richText, "color" .= color]+          <> childrenPairs children+    )+  NumberedListItemBlock {..} ->+    ( "numbered_list_item",+      object $+        ["rich_text" .= richText, "color" .= color]+          <> maybe [] (\f -> ["list_format" .= f]) listFormat+          <> maybe [] (\i -> ["list_start_index" .= i]) listStartIndex+          <> childrenPairs children+    )+  ToDoBlock {..} ->+    ( "to_do",+      object $+        ["rich_text" .= richText, "color" .= color, "checked" .= checked]+          <> childrenPairs children+    )+  ToggleBlock {..} ->+    ( "toggle",+      object $+        ["rich_text" .= richText, "color" .= color]+          <> childrenPairs children+    )+  QuoteBlock {..} ->+    ( "quote",+      object $+        ["rich_text" .= richText, "color" .= color]+          <> childrenPairs children+    )+  CalloutBlock {..} ->+    ( "callout",+      object $+        ["rich_text" .= richText, "color" .= color]+          <> maybe [] (\i -> ["icon" .= i]) calloutIcon+          <> childrenPairs children+    )+  CodeBlock {..} ->+    ("code", object ["rich_text" .= richText, "caption" .= caption, "language" .= language])+  EquationBlock {..} ->+    ("equation", object ["expression" .= expression])+  ImageBlock {..} ->+    ("image", object $ fileSourcePairs imageSource <> ["caption" .= caption])+  VideoBlock {..} ->+    ("video", object $ fileSourcePairs videoSource <> ["caption" .= caption])+  AudioBlock {..} ->+    ("audio", object $ fileSourcePairs audioSource)+  FileBlock {..} ->+    ( "file",+      object $+        fileSourcePairs fileSource+          <> ["caption" .= caption]+          <> maybe [] (\n -> ["name" .= n]) fileName+    )+  PdfBlock {..} ->+    ("pdf", object $ fileSourcePairs pdfSource <> ["caption" .= caption])+  BookmarkBlock {..} ->+    ("bookmark", object ["url" .= url, "caption" .= caption])+  EmbedBlock {..} ->+    ("embed", object ["url" .= url])+  LinkToPageBlock {..} ->+    ("link_to_page", toJSON linkTarget)+  LinkPreviewBlock {..} ->+    ("link_preview", object ["url" .= url])+  DividerBlock ->+    ("divider", object [])+  BreadcrumbBlock ->+    ("breadcrumb", object [])+  TableOfContentsBlock {..} ->+    ("table_of_contents", object ["color" .= color])+  ColumnListBlock {..} ->+    ("column_list", object $ childrenPairs children)+  ColumnBlock {..} ->+    ("column", object $ childrenPairs children)+  TableBlock {..} ->+    ( "table",+      object $+        ["table_width" .= tableWidth, "has_column_header" .= hasColumnHeader, "has_row_header" .= hasRowHeader]+          <> childrenPairs children+    )+  TableRowBlock {..} ->+    ("table_row", object ["cells" .= cells])+  ChildPageBlock {..} ->+    ("child_page", object ["title" .= title])+  ChildDatabaseBlock {..} ->+    ("child_database", object ["title" .= title])+  SyncedBlockContent {..} ->+    ( "synced_block",+      object $+        ["synced_from" .= syncedFrom]+          <> childrenPairs children+    )+  UnsupportedBlock ->+    ("unsupported", object [])+  UnknownBlock typeName val ->+    (typeName, val)++-- ---------------------------------------------------------------------------+-- JSON instances+-- ---------------------------------------------------------------------------++-- | Parse block content from a type name and the inner JSON value (the value+-- under the type key). This is called by 'BlockObject'\'s 'FromJSON' instance+-- and by the standalone 'FromJSON BlockContent'.+parseBlockContent :: Text -> Value -> Parser BlockContent+parseBlockContent typeName val = case typeName of+  "paragraph" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    color <- fromMaybe Default <$> o .:? "color"+    paragraphIcon <- o .:? "icon"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure ParagraphBlock {..}+  "heading_1" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    color <- fromMaybe Default <$> o .:? "color"+    isToggleable <- fromMaybe False <$> o .:? "is_toggleable"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure Heading1Block {..}+  "heading_2" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    color <- fromMaybe Default <$> o .:? "color"+    isToggleable <- fromMaybe False <$> o .:? "is_toggleable"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure Heading2Block {..}+  "heading_3" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    color <- fromMaybe Default <$> o .:? "color"+    isToggleable <- fromMaybe False <$> o .:? "is_toggleable"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure Heading3Block {..}+  "bulleted_list_item" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    color <- fromMaybe Default <$> o .:? "color"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure BulletedListItemBlock {..}+  "numbered_list_item" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    color <- fromMaybe Default <$> o .:? "color"+    listFormat <- o .:? "list_format"+    listStartIndex <- o .:? "list_start_index"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure NumberedListItemBlock {..}+  "to_do" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    color <- fromMaybe Default <$> o .:? "color"+    checked <- fromMaybe False <$> o .:? "checked"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure ToDoBlock {..}+  "toggle" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    color <- fromMaybe Default <$> o .:? "color"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure ToggleBlock {..}+  "quote" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    color <- fromMaybe Default <$> o .:? "color"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure QuoteBlock {..}+  "callout" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    color <- fromMaybe Default <$> o .:? "color"+    calloutIcon <- o .:? "icon"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure CalloutBlock {..}+  "code" -> parseObj $ \o -> do+    richText <- o .: "rich_text"+    caption <- fromMaybe Vector.empty <$> o .:? "caption"+    language <- o .: "language"+    pure CodeBlock {..}+  "equation" -> parseObj $ \o -> do+    expression <- o .: "expression"+    pure EquationBlock {..}+  "image" -> parseObj $ \o -> do+    imageSource <- parseFileSource o+    caption <- fromMaybe Vector.empty <$> o .:? "caption"+    pure ImageBlock {..}+  "video" -> parseObj $ \o -> do+    videoSource <- parseFileSource o+    caption <- fromMaybe Vector.empty <$> o .:? "caption"+    pure VideoBlock {..}+  "audio" -> parseObj $ \o -> do+    audioSource <- parseFileSource o+    pure AudioBlock {..}+  "file" -> parseObj $ \o -> do+    fileSource <- parseFileSource o+    caption <- fromMaybe Vector.empty <$> o .:? "caption"+    fileName <- o .:? "name"+    pure FileBlock {..}+  "pdf" -> parseObj $ \o -> do+    pdfSource <- parseFileSource o+    caption <- fromMaybe Vector.empty <$> o .:? "caption"+    pure PdfBlock {..}+  "bookmark" -> parseObj $ \o -> do+    url <- o .: "url"+    caption <- fromMaybe Vector.empty <$> o .:? "caption"+    pure BookmarkBlock {..}+  "embed" -> parseObj $ \o -> do+    url <- o .: "url"+    pure EmbedBlock {..}+  "link_to_page" -> do+    linkTarget <- Aeson.parseJSON val+    pure LinkToPageBlock {..}+  "link_preview" -> parseObj $ \o -> do+    url <- o .: "url"+    pure LinkPreviewBlock {..}+  "divider" -> pure DividerBlock+  "breadcrumb" -> pure BreadcrumbBlock+  "table_of_contents" -> parseObj $ \o -> do+    color <- fromMaybe Default <$> o .:? "color"+    pure TableOfContentsBlock {..}+  "column_list" -> parseObj $ \o -> do+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure ColumnListBlock {..}+  "column" -> parseObj $ \o -> do+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure ColumnBlock {..}+  "table" -> parseObj $ \o -> do+    tableWidth <- o .: "table_width"+    hasColumnHeader <- o .: "has_column_header"+    hasRowHeader <- o .: "has_row_header"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure TableBlock {..}+  "table_row" -> parseObj $ \o -> do+    cells <- o .: "cells"+    pure TableRowBlock {..}+  "child_page" -> parseObj $ \o -> do+    title <- o .: "title"+    pure ChildPageBlock {..}+  "child_database" -> parseObj $ \o -> do+    title <- o .: "title"+    pure ChildDatabaseBlock {..}+  "synced_block" -> parseObj $ \o -> do+    syncedFrom <- o .: "synced_from"+    children <- fromMaybe Vector.empty <$> o .:? "children"+    pure SyncedBlockContent {..}+  "unsupported" -> pure UnsupportedBlock+  _ -> pure (UnknownBlock typeName val)+  where+    parseObj :: (Aeson.Object -> Parser BlockContent) -> Parser BlockContent+    parseObj f = case val of+      Object o -> f o+      _ -> fail $ "Expected object for block type " <> unpack typeName++instance FromJSON BlockContent where+  parseJSON = \case+    Object o -> do+      typeName <- o .: "type"+      contentVal <- o .: Key.fromText typeName+      parseBlockContent typeName contentVal+    _ -> fail "Expected object for BlockContent"++instance ToJSON BlockContent where+  toJSON bc =+    let (typeName, inner) = blockContentFields bc+     in object ["type" .= typeName, Key.fromText typeName .= inner]++-- ---------------------------------------------------------------------------+-- BlockUpdate+-- ---------------------------------------------------------------------------++-- | Wrapper for block content used in the update (PATCH) endpoint.+--+-- Serializes without the @\"type\"@ key — only the type-named key with inner+-- content:+--+-- @+-- { "paragraph": { "rich_text": [...] } }+-- @+newtype BlockUpdate = BlockUpdate BlockContent+  deriving stock (Show)++instance ToJSON BlockUpdate where+  toJSON (BlockUpdate bc) =+    let (typeName, inner) = blockContentFields bc+     in object [Key.fromText typeName .= inner]++-- ---------------------------------------------------------------------------+-- Smart constructors+-- ---------------------------------------------------------------------------++-- | Build a 'RichText' vector containing a single plain-text segment.+-- This is the most common way to create block content.+mkRichText :: Text -> Vector RichText+mkRichText t =+  Vector.singleton+    RichText+      { plainText = t,+        href = Nothing,+        annotations = defaultAnnotations,+        type_ = "text",+        content = TextContentWrapper (TextContent {content = t, link = Nothing})+      }++-- | Create a paragraph block from plain text.+textBlock :: Text -> BlockContent+textBlock = paragraphBlock . mkRichText++-- | Create a paragraph block.+paragraphBlock :: Vector RichText -> BlockContent+paragraphBlock rt = ParagraphBlock rt Default Nothing Vector.empty++-- | Create a heading block at the given level (1, 2, or 3; defaults to 3).+headingBlock :: Int -> Vector RichText -> BlockContent+headingBlock 1 rt = Heading1Block rt Default False Vector.empty+headingBlock 2 rt = Heading2Block rt Default False Vector.empty+headingBlock _ rt = Heading3Block rt Default False Vector.empty++-- | Create a bulleted list item block.+bulletedListItemBlock :: Vector RichText -> BlockContent+bulletedListItemBlock rt = BulletedListItemBlock rt Default Vector.empty++-- | Create a numbered list item block.+numberedListItemBlock :: Vector RichText -> BlockContent+numberedListItemBlock rt = NumberedListItemBlock rt Default Nothing Nothing Vector.empty++-- | Create a to-do block.+toDoBlock :: Vector RichText -> Bool -> BlockContent+toDoBlock rt isChecked = ToDoBlock rt Default isChecked Vector.empty++-- | Create a toggle block.+toggleBlock :: Vector RichText -> BlockContent+toggleBlock rt = ToggleBlock rt Default Vector.empty++-- | Create a quote block.+quoteBlock :: Vector RichText -> BlockContent+quoteBlock rt = QuoteBlock rt Default Vector.empty++-- | Create a callout block with an optional icon.+calloutBlock :: Vector RichText -> Maybe Icon -> BlockContent+calloutBlock rt icon = CalloutBlock rt Default icon Vector.empty++-- | Create a code block with a language.+codeBlock :: Vector RichText -> CodeLanguage -> BlockContent+codeBlock rt lang = CodeBlock rt Vector.empty lang++-- | Create an equation block from a KaTeX expression.+equationBlock :: Text -> BlockContent+equationBlock = EquationBlock++-- | Create a bookmark block from a URL.+bookmarkBlock :: Text -> BlockContent+bookmarkBlock u = BookmarkBlock u Vector.empty++-- | Create a divider block.+dividerBlock :: BlockContent+dividerBlock = DividerBlock++-- | Create an image block from a file source.+imageBlock :: FileSource -> BlockContent+imageBlock src = ImageBlock src Vector.empty++-- | Attach children to a block. For constructors that do not support+-- children, the block is returned unchanged.+withChildren :: BlockContent -> Vector BlockContent -> BlockContent+withChildren block cs = case block of+  ParagraphBlock {} -> block {children = cs}+  Heading1Block {} -> block {children = cs}+  Heading2Block {} -> block {children = cs}+  Heading3Block {} -> block {children = cs}+  BulletedListItemBlock {} -> block {children = cs}+  NumberedListItemBlock {} -> block {children = cs}+  ToDoBlock {} -> block {children = cs}+  ToggleBlock {} -> block {children = cs}+  QuoteBlock {} -> block {children = cs}+  CalloutBlock {} -> block {children = cs}+  ColumnListBlock {} -> block {children = cs}+  ColumnBlock {} -> block {children = cs}+  TableBlock {} -> block {children = cs}+  SyncedBlockContent {} -> block {children = cs}+  _ -> block
src/Notion/V1/Blocks.hs view
@@ -3,10 +3,13 @@   ( -- * Main types     BlockID,     BlockObject (..),-    BlockContent (..),+    BlockUpdate (..),     AppendBlockChildren (..),     Position (..), +    -- * Re-exported from BlockContent+    module Notion.V1.BlockContent,+     -- * Servant     API,   )@@ -17,6 +20,7 @@ import Data.Aeson qualified as Aeson import Data.Aeson.Key qualified as Key import Notion.Prelude+import Notion.V1.BlockContent import Notion.V1.Common (BlockID, ObjectType (..), Parent, UUID (..)) import Notion.V1.ListOf (ListOf) import Notion.V1.Users (UserReference)@@ -33,7 +37,7 @@     hasChildren :: Bool,     inTrash :: Bool,     type_ :: Text,-    content :: Value,+    content :: BlockContent,     object :: ObjectType   }   deriving stock (Generic, Show)@@ -52,8 +56,8 @@       hasChildren <- o .: "has_children"       inTrash <- (o .: "in_trash") <|> (o .: "is_archived") <|> (o .: "archived") <|> pure False       type_ <- o .: "type"-      -- Content is stored under a field named after the block type (e.g., "heading_1", "paragraph")-      content <- o .: Key.fromText type_+      contentVal <- o .: Key.fromText type_+      content <- parseBlockContent type_ contentVal       object <- o .: "object"       return BlockObject {..}     _ -> fail "Expected object for BlockObject"@@ -70,19 +74,10 @@         "has_children" .= hasChildren,         "in_trash" .= inTrash,         "type" .= type_,-        Key.fromText type_ .= content,+        Key.fromText type_ .= snd (blockContentFields content),         "object" .= object       ] --- | Block content for update-newtype BlockContent = BlockContent-  { content :: Value-  }-  deriving stock (Generic, Show)--instance ToJSON BlockContent where-  toJSON = genericToJSON aesonOptions- -- | Block insertion position for the Append Block Children endpoint. -- -- In API version 2026-03-11, the old @after@ parameter was replaced by a@@ -115,7 +110,7 @@  -- | Append children to a block data AppendBlockChildren = AppendBlockChildren-  { children :: Vector Value,+  { children :: Vector BlockContent,     position :: Maybe Position   }   deriving stock (Generic, Show)@@ -132,7 +127,7 @@     :> ( Capture "block_id" BlockID            :> Get '[JSON] BlockObject            :<|> Capture "block_id" BlockID-           :> ReqBody '[JSON] BlockContent+           :> ReqBody '[JSON] BlockUpdate            :> Patch '[JSON] BlockObject            :<|> Capture "block_id" BlockID            :> "children"
src/Notion/V1/Common.hs view
@@ -132,7 +132,7 @@     NativeIcon {iconName :: Text, iconColor :: Maybe Text}   | -- | Custom emoji icon specified by ID     CustomEmojiIcon {customEmojiId :: UUID}-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON Icon where   parseJSON = \case@@ -158,7 +158,7 @@ data Cover   = FileCover {file :: File}   | ExternalCover {external :: ExternalFile}-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON Cover where   parseJSON = \case@@ -179,19 +179,30 @@   { url :: Text,     expiryTime :: Maybe POSIXTime   }-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON File where-  parseJSON = genericParseJSON aesonOptions+  parseJSON = \case+    Object o -> do+      url <- o .: "url"+      mExpiry <- o .:? "expiry_time"+      expiryTime <- case mExpiry of+        Nothing -> pure Nothing+        Just str -> Just <$> parseISO8601 str+      pure File {..}+    _ -> fail "Expected object for File"  instance ToJSON File where-  toJSON = genericToJSON aesonOptions+  toJSON File {..} =+    object $+      ["url" .= url]+        <> maybe [] (\t -> ["expiry_time" .= posixToISO8601 t]) expiryTime  -- | External file object newtype ExternalFile = ExternalFile   { url :: Text   }-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON ExternalFile where   parseJSON = genericParseJSON aesonOptions
src/Notion/V1/Pages.hs view
@@ -37,6 +37,7 @@ import Data.Aeson qualified as Aeson import Data.Aeson.KeyMap qualified as KeyMap import Notion.Prelude+import Notion.V1.BlockContent (BlockContent) import Notion.V1.Blocks (Position) import Notion.V1.Common (Cover, Icon, ObjectType (..), Parent, UUID) import Notion.V1.ListOf (ListOf)@@ -136,7 +137,7 @@ data CreatePage = CreatePage   { parent :: Parent,     properties :: PageProperties,-    children :: Maybe (Vector Value),+    children :: Maybe (Vector BlockContent),     markdown :: Maybe Text,     icon :: Maybe Icon,     cover :: Maybe Cover,
src/Notion/V1/RichText.hs view
@@ -26,7 +26,7 @@     type_ :: Text,     content :: RichTextContent   }-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON RichText where   parseJSON = \case@@ -61,7 +61,7 @@   { content :: Text,     link :: Maybe Link   }-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON TextContent where   parseJSON = genericParseJSON aesonOptions@@ -83,7 +83,7 @@   | DatabaseMention {database :: UUID}   | DateMention {date :: Date}   | LinkPreviewMention {url :: Text}-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON MentionContent where   parseJSON = \case@@ -130,7 +130,7 @@ newtype EquationContent = EquationContent   { expression :: Text   }-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON EquationContent where   parseJSON = genericParseJSON aesonOptions@@ -143,7 +143,7 @@   = TextContentWrapper TextContent   | MentionContentWrapper MentionContent   | EquationContentWrapper EquationContent-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  -- | Text annotations data Annotations = Annotations@@ -154,7 +154,7 @@     code :: Bool,     color :: Color   }-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON Annotations where   parseJSON = genericParseJSON aesonOptions@@ -178,7 +178,7 @@ newtype Link = Link   { url :: Text   }-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON Link where   parseJSON = genericParseJSON aesonOptions@@ -192,7 +192,7 @@     end :: Maybe Text,     timeZone :: Maybe TimeZone   }-  deriving stock (Generic, Show)+  deriving stock (Eq, Generic, Show)  instance FromJSON Date where   parseJSON = genericParseJSON aesonOptions@@ -204,4 +204,4 @@ newtype TimeZone = TimeZone   { text :: Text   }-  deriving newtype (FromJSON, IsString, Show, ToJSON)+  deriving newtype (Eq, FromJSON, IsString, Show, ToJSON)
tasty/Main.hs view
@@ -9,11 +9,12 @@ import Data.Text qualified as Text import Data.Vector qualified as Vector import Notion.V1+import Notion.V1.BlockContent (BlockContent (..), CodeLanguage (..), FileSource (..), SyncedFrom (..), blockContentType, bookmarkBlock, bulletedListItemBlock, calloutBlock, codeBlock, dividerBlock, headingBlock, imageBlock, mkRichText, numberedListItemBlock, paragraphBlock, quoteBlock, textBlock, toDoBlock, toggleBlock, withChildren) import Notion.V1.Blocks (AppendBlockChildren (..), BlockObject (..), Position (..)) import Notion.V1.Blocks qualified as Blocks import Notion.V1.Comments (CommentObject (..), CreateComment (..)) import Notion.V1.Comments qualified as Comments-import Notion.V1.Common (Icon (..), Parent (..), UUID (..))+import Notion.V1.Common (Color (..), ExternalFile (..), Icon (..), Parent (..), UUID (..)) import Notion.V1.CustomEmojis (CustomEmoji (..)) import Notion.V1.DataSources (DataSourceObject (..)) import Notion.V1.DataSources qualified as DataSources@@ -170,28 +171,13 @@   clientEnv <- getClientEnv "https://api.notion.com/v1"   pure $ makeMethods clientEnv (Text.pack token) --- | Helper to make a rich text array from a plain string-mkRichTextValue :: Text.Text -> Aeson.Value-mkRichTextValue t =-  Aeson.Array . Vector.singleton $-    Aeson.object [("text", Aeson.object [("content", Aeson.String t)])]---- | Helper to make a paragraph block JSON value-mkParagraphBlock :: Text.Text -> Aeson.Value-mkParagraphBlock t =-  Aeson.object-    [ ("type", Aeson.String "paragraph"),-      ("paragraph", Aeson.object [("rich_text", mkRichTextValue t)])-    ]+-- | Helper to make a paragraph block+mkParagraphBlock :: Text.Text -> BlockContent+mkParagraphBlock = textBlock --- | Helper to make a heading block JSON value-mkHeadingBlock :: Text.Text -> Int -> Aeson.Value-mkHeadingBlock t level =-  let headingType = "heading_" <> Text.pack (show level)-   in Aeson.object-        [ ("type", Aeson.String headingType),-          (Key.fromText headingType, Aeson.object [("rich_text", mkRichTextValue t)])-        ]+-- | Helper to make a heading block+mkHeadingBlock :: Text.Text -> Int -> BlockContent+mkHeadingBlock t level = headingBlock level (mkRichText t)  -- | Helper to create a plain RichText from a string mkPlainRichText :: Text.Text -> RichText@@ -249,7 +235,34 @@       testCase "Parse PageObject with in_trash" testParsePageObject,       testCase "Parse NotionError from JSON" testParseNotionError,       testCase "Parse DatabaseObject with in_trash" testParseDatabaseObject,-      testCase "Parse DataSourceObject with in_trash" testParseDataSourceObject+      testCase "Parse DataSourceObject with in_trash" testParseDataSourceObject,+      testCase "BlockContent round-trip: paragraph" testBlockContentParagraph,+      testCase "BlockContent round-trip: heading with is_toggleable" testBlockContentHeading,+      testCase "BlockContent round-trip: code block" testBlockContentCode,+      testCase "BlockContent round-trip: image (external)" testBlockContentImage,+      testCase "BlockContent round-trip: divider" testBlockContentDivider,+      testCase "BlockContent round-trip: table" testBlockContentTable,+      testCase "BlockContent round-trip: bookmark" testBlockContentBookmark,+      testCase "BlockContent round-trip: unknown block" testBlockContentUnknown,+      testCase "BlockContent round-trip: toggle with nested children" testBlockContentNestedToggle,+      testCase "BlockContent round-trip: column list with columns" testBlockContentNestedColumnList,+      testCase "BlockContent round-trip: paragraph with children" testBlockContentNestedParagraph,+      testCase "BlockContent round-trip: heading (toggleable) with children" testBlockContentNestedHeading,+      testCase "BlockContent round-trip: bulleted list with children" testBlockContentNestedBulletedList,+      testCase "BlockContent round-trip: numbered list with children" testBlockContentNestedNumberedList,+      testCase "BlockContent round-trip: to-do with children" testBlockContentNestedToDo,+      testCase "BlockContent round-trip: quote with children" testBlockContentNestedQuote,+      testCase "BlockContent round-trip: callout with children" testBlockContentNestedCallout,+      testCase "BlockContent round-trip: table with row children" testBlockContentNestedTable,+      testCase "BlockContent round-trip: synced block with children" testBlockContentNestedSyncedBlock,+      testCase "BlockContent round-trip: two-level nesting" testBlockContentTwoLevelNesting,+      testCase "BlockContent round-trip: withChildren combinator" testBlockContentWithChildren,+      testCase "BlockContent: withChildren no-op on non-child types" testWithChildrenNoOp,+      testCase "BlockContent: childless blocks omit children key" testBlockContentNoChildrenKey,+      testCase "BlockContent: blocks with children include children key" testBlockContentHasChildrenKey,+      testCase "BlockUpdate with nested children" testBlockUpdateWithChildren,+      testCase "BlockContent: parse from raw JSON with children" testParseBlockContentWithChildren,+      testCase "BlockUpdate omits type key" testBlockUpdateSerialization     ]  testParseBlockObject :: Assertion@@ -269,6 +282,9 @@     Right block -> do       assertEqual "inTrash should be False" False (Notion.V1.Blocks.inTrash block)       assertEqual "type should be paragraph" "paragraph" (Notion.V1.Blocks.type_ block)+      case Blocks.content block of+        ParagraphBlock {richText} -> assertEqual "rich_text should be empty" Vector.empty richText+        other -> assertFailure $ "Expected ParagraphBlock, got: " <> show other  testParseBlockObjectLegacy :: Assertion testParseBlockObjectLegacy = do@@ -354,6 +370,251 @@       assertEqual "message" "The provided page ID is not a valid UUID." errMessage  -- =====================================================================+-- BlockContent Round-trip Tests+-- =====================================================================++-- | Helper: encode then decode, verify equality.+roundTrip :: BlockContent -> Assertion+roundTrip original =+  case Aeson.fromJSON (Aeson.toJSON original) of+    Aeson.Error err -> assertFailure $ "Round-trip failed: " <> err+    Aeson.Success parsed -> assertEqual "round-trip" original parsed++testBlockContentParagraph :: Assertion+testBlockContentParagraph = roundTrip $ paragraphBlock (mkRichText "Hello world")++testBlockContentHeading :: Assertion+testBlockContentHeading = roundTrip $ Heading1Block (mkRichText "Title") Default True Vector.empty++testBlockContentCode :: Assertion+testBlockContentCode = roundTrip $ codeBlock (mkRichText "main = pure ()") Haskell++testBlockContentImage :: Assertion+testBlockContentImage = roundTrip $ imageBlock (ExternalSource (ExternalFile "https://example.com/img.png"))++testBlockContentDivider :: Assertion+testBlockContentDivider = roundTrip DividerBlock++testBlockContentTable :: Assertion+testBlockContentTable = roundTrip $ TableBlock 3 True False Vector.empty++testBlockContentBookmark :: Assertion+testBlockContentBookmark = roundTrip $ bookmarkBlock "https://haskell.org"++testBlockContentUnknown :: Assertion+testBlockContentUnknown = roundTrip $ UnknownBlock "custom_widget" (Aeson.object [("data", Aeson.String "test")])++testBlockContentNestedToggle :: Assertion+testBlockContentNestedToggle =+  roundTrip $+    ToggleBlock+      { richText = mkRichText "Click to expand",+        color = Default,+        children =+          Vector.fromList+            [ textBlock "First child",+              textBlock "Second child"+            ]+      }++testBlockContentNestedColumnList :: Assertion+testBlockContentNestedColumnList =+  roundTrip $+    ColumnListBlock+      { children =+          Vector.fromList+            [ ColumnBlock {children = Vector.singleton (textBlock "Column 1 content")},+              ColumnBlock {children = Vector.singleton (textBlock "Column 2 content")}+            ]+      }++testBlockContentWithChildren :: Assertion+testBlockContentWithChildren = do+  let block = toggleBlock (mkRichText "Toggle") `withChildren` Vector.fromList [textBlock "Inner"]+  roundTrip block+  case block of+    ToggleBlock {children} ->+      assertEqual "should have 1 child" 1 (Vector.length children)+    other -> assertFailure $ "Expected ToggleBlock, got: " <> show other++testBlockContentNestedParagraph :: Assertion+testBlockContentNestedParagraph =+  roundTrip $+    paragraphBlock (mkRichText "Parent paragraph")+      `withChildren` Vector.singleton (textBlock "Child of paragraph")++testBlockContentNestedHeading :: Assertion+testBlockContentNestedHeading =+  roundTrip $+    Heading1Block+      { richText = mkRichText "Toggleable heading",+        color = Default,+        isToggleable = True,+        children = Vector.singleton (textBlock "Under the heading")+      }++testBlockContentNestedBulletedList :: Assertion+testBlockContentNestedBulletedList =+  roundTrip $+    bulletedListItemBlock (mkRichText "Parent bullet")+      `withChildren` Vector.fromList+        [ bulletedListItemBlock (mkRichText "Sub-bullet A"),+          bulletedListItemBlock (mkRichText "Sub-bullet B")+        ]++testBlockContentNestedNumberedList :: Assertion+testBlockContentNestedNumberedList =+  roundTrip $+    numberedListItemBlock (mkRichText "Parent numbered")+      `withChildren` Vector.fromList+        [ numberedListItemBlock (mkRichText "Sub 1"),+          numberedListItemBlock (mkRichText "Sub 2")+        ]++testBlockContentNestedToDo :: Assertion+testBlockContentNestedToDo =+  roundTrip $+    toDoBlock (mkRichText "Task with sub-tasks") False+      `withChildren` Vector.fromList+        [ toDoBlock (mkRichText "Sub-task done") True,+          toDoBlock (mkRichText "Sub-task pending") False+        ]++testBlockContentNestedQuote :: Assertion+testBlockContentNestedQuote =+  roundTrip $+    quoteBlock (mkRichText "A quote")+      `withChildren` Vector.singleton (textBlock "Attribution line")++testBlockContentNestedCallout :: Assertion+testBlockContentNestedCallout =+  roundTrip $+    calloutBlock (mkRichText "Notice") (Just (EmojiIcon "⚠️"))+      `withChildren` Vector.fromList+        [ textBlock "Detail one",+          textBlock "Detail two"+        ]++testBlockContentNestedTable :: Assertion+testBlockContentNestedTable =+  roundTrip $+    TableBlock+      { tableWidth = 2,+        hasColumnHeader = True,+        hasRowHeader = False,+        children =+          Vector.fromList+            [ TableRowBlock {cells = Vector.fromList [mkRichText "A", mkRichText "B"]},+              TableRowBlock {cells = Vector.fromList [mkRichText "1", mkRichText "2"]}+            ]+      }++testBlockContentNestedSyncedBlock :: Assertion+testBlockContentNestedSyncedBlock =+  roundTrip $+    SyncedBlockContent+      { syncedFrom = SyncedOriginal,+        children = Vector.fromList [textBlock "Synced content", bulletedListItemBlock (mkRichText "Synced item")]+      }++testBlockContentTwoLevelNesting :: Assertion+testBlockContentTwoLevelNesting =+  roundTrip $+    toggleBlock (mkRichText "Outer")+      `withChildren` Vector.fromList+        [ quoteBlock (mkRichText "Middle")+            `withChildren` Vector.singleton (textBlock "Deepest"),+          textBlock "Also in outer"+        ]++testWithChildrenNoOp :: Assertion+testWithChildrenNoOp = do+  let code = codeBlock (mkRichText "x = 1") Haskell+      codeWithKids = code `withChildren` Vector.singleton (textBlock "ignored")+  assertEqual "withChildren should be no-op on CodeBlock" code codeWithKids++  let div = dividerBlock+      divWithKids = div `withChildren` Vector.singleton (textBlock "ignored")+  assertEqual "withChildren should be no-op on DividerBlock" div divWithKids++  let bk = bookmarkBlock "https://example.com"+      bkWithKids = bk `withChildren` Vector.singleton (textBlock "ignored")+  assertEqual "withChildren should be no-op on BookmarkBlock" bk bkWithKids++testBlockContentNoChildrenKey :: Assertion+testBlockContentNoChildrenKey = do+  let json = Aeson.toJSON (paragraphBlock (mkRichText "No kids"))+  case json of+    Aeson.Object o -> case KeyMap.lookup "paragraph" o of+      Just (Aeson.Object inner) ->+        assertBool "childless block should not have 'children' key" (not $ KeyMap.member "children" inner)+      _ -> assertFailure "Expected paragraph object"+    _ -> assertFailure "Expected object"++testBlockContentHasChildrenKey :: Assertion+testBlockContentHasChildrenKey = do+  let block = toggleBlock (mkRichText "T") `withChildren` Vector.singleton (textBlock "C")+      json = Aeson.toJSON block+  case json of+    Aeson.Object o -> case KeyMap.lookup "toggle" o of+      Just (Aeson.Object inner) -> do+        assertBool "block with children should have 'children' key" (KeyMap.member "children" inner)+        case KeyMap.lookup "children" inner of+          Just (Aeson.Array arr) ->+            assertEqual "children array should have 1 element" 1 (Vector.length arr)+          _ -> assertFailure "Expected children to be an array"+      _ -> assertFailure "Expected toggle object"+    _ -> assertFailure "Expected object"++testBlockUpdateWithChildren :: Assertion+testBlockUpdateWithChildren = do+  let block = toggleBlock (mkRichText "T") `withChildren` Vector.singleton (textBlock "C")+      update = Blocks.BlockUpdate block+      json = Aeson.toJSON update+  case json of+    Aeson.Object o -> do+      assertBool "should have 'toggle' key" (KeyMap.member "toggle" o)+      assertBool "should NOT have 'type' key" (not $ KeyMap.member "type" o)+      case KeyMap.lookup "toggle" o of+        Just (Aeson.Object inner) ->+          assertBool "update should include 'children' key" (KeyMap.member "children" inner)+        _ -> assertFailure "Expected toggle object in update"+    _ -> assertFailure "Expected object from BlockUpdate ToJSON"++testParseBlockContentWithChildren :: Assertion+testParseBlockContentWithChildren = do+  let json =+        "{\"type\":\"toggle\",\"toggle\":{\"rich_text\":[{\"type\":\"text\",\"text\":"+          <> "{\"content\":\"Click\",\"link\":null},\"annotations\":{\"bold\":false,"+          <> "\"italic\":false,\"strikethrough\":false,\"underline\":false,\"code\":false,"+          <> "\"color\":\"default\"},\"plain_text\":\"Click\",\"href\":null}],"+          <> "\"color\":\"default\",\"children\":[{\"type\":\"paragraph\",\"paragraph\":"+          <> "{\"rich_text\":[{\"type\":\"text\",\"text\":{\"content\":\"Inner\",\"link\":null},"+          <> "\"annotations\":{\"bold\":false,\"italic\":false,\"strikethrough\":false,"+          <> "\"underline\":false,\"code\":false,\"color\":\"default\"},\"plain_text\":\"Inner\","+          <> "\"href\":null}],\"color\":\"default\"}}]}}"+  case Aeson.eitherDecode json of+    Left err -> assertFailure $ "Failed to parse: " <> err+    Right block -> case (block :: BlockContent) of+      ToggleBlock {children} -> do+        assertEqual "should have 1 child" 1 (Vector.length children)+        case Vector.head children of+          ParagraphBlock {richText} ->+            assertBool "child should have rich text" (not $ Vector.null richText)+          other -> assertFailure $ "Expected ParagraphBlock child, got: " <> show other+      other -> assertFailure $ "Expected ToggleBlock, got: " <> show other++testBlockUpdateSerialization :: Assertion+testBlockUpdateSerialization = do+  let update = Blocks.BlockUpdate (paragraphBlock (mkRichText "Updated"))+      json = Aeson.toJSON update+  case json of+    Aeson.Object o -> do+      assertBool "should have 'paragraph' key" (KeyMap.member "paragraph" o)+      assertBool "should NOT have 'type' key" (not $ KeyMap.member "type" o)+    _ -> assertFailure "Expected object from BlockUpdate ToJSON"++-- ===================================================================== -- JSON Serialization Tests (unit tests, no API token needed) -- ===================================================================== @@ -806,14 +1067,8 @@           [ mkHeadingBlock "Test Heading" 1,             mkParagraphBlock "First paragraph content.",             mkParagraphBlock "Second paragraph content.",-            Aeson.object-              [ ("type", Aeson.String "bulleted_list_item"),-                ("bulleted_list_item", Aeson.object [("rich_text", mkRichTextValue "List item one")])-              ],-            Aeson.object-              [ ("type", Aeson.String "bulleted_list_item"),-                ("bulleted_list_item", Aeson.object [("rich_text", mkRichTextValue "List item two")])-              ]+            bulletedListItemBlock (mkRichText "List item one"),+            bulletedListItemBlock (mkRichText "List item two")           ]       appendReq = AppendBlockChildren {children = blocks, position = Nothing}   appendResult <- appendBlockChildren pageId appendReq@@ -829,6 +1084,19 @@       BlockObject {id = blockId} = firstBlock   retrieved <- retrieveBlock blockId   assertEqual "Retrieved block ID should match" blockId (Notion.V1.Blocks.id retrieved)++  -- Append a toggle block with nested children+  let nestedToggle =+        toggleBlock (mkRichText "Toggle with children")+          `withChildren` Vector.fromList [textBlock "Child paragraph 1", textBlock "Child paragraph 2"]+      nestedReq = AppendBlockChildren {children = Vector.singleton nestedToggle, position = Nothing}+  nestedResult <- appendBlockChildren pageId nestedReq+  let toggleObj = Vector.head (results nestedResult)+  assertBool "Toggle should have children" (Blocks.hasChildren toggleObj)++  -- Fetch the toggle's children+  toggleChildren <- listBlockChildren (Blocks.id toggleObj) Nothing Nothing+  assertEqual "Toggle should have 2 children" 2 (Vector.length $ results toggleChildren)    -- Append a block at the start position   let startBlock = mkParagraphBlock "Inserted at start."