notion-client-effectful
An effectful surface for
notion-client.
This package wraps the Notion.V1.Methods record as a dynamic
effect — so code written against effectful does not have to
thread the Methods value through every call site and does not
have to drop into IO at each API boundary.
Purpose
notion-client exposes a single Methods record whose fields
are IO-typed API calls. That is ergonomic for a plain-IO
program but awkward for an effectful-based caller: every call
requires liftIO and escapes the ambient Error / Reader /
Log stack.
notion-client-effectful gives that caller an equivalent set of
operations surfaced as an Effect so the stack is preserved.
Import pattern
Every smart constructor shares its name with the matching
Notion.V1.Methods record selector. That is on purpose — it
keeps migration from IO mechanical — but it means a file that
imports both modules unqualified will see name clashes. Import
one of the two qualified:
import Notion.V1 (Methods, getClientEnv, makeMethods)
import Notion.V1.Effectful qualified as NE
Then NE.retrievePage, NE.search, NE.runNotion, and so on.
Error handling
runNotion catches Notion.V1.Error.NotionError thrown by the
underlying Methods and re-raises it via the Error NotionError
effect, so callers can branch on API error shapes
(object_not_found, validation failures, etc.) without reaching
for IO-level exception handling.
Other Servant.Client.ClientError values — network failures,
decoding errors — are not caught and remain IO exceptions.
This preserves the contract of the underlying notion-client
library and lets callers layer their own Error ClientError
interpretation on top later. The same applies to
UnknownHTTPResponseError, RequestTimeoutError and
InvalidPathParameterError from Notion.V1.Error. Retries of
rate-limited requests happen inside Methods, before an error reaches
runNotion.
Minimum viable usage
module Demo where
import Data.Text (Text)
import Data.Text qualified as Text
import Effectful (Eff, runEff, (:>))
import Effectful.Error.Static (Error, runErrorNoCallStack)
import Notion.V1 (getClientEnv, makeMethods)
import Notion.V1.Common (UUID (..))
import Notion.V1.Effectful qualified as NE
import Notion.V1.Error (NotionError)
import System.Environment qualified as Env
demo ::
(NE.Notion :> es, Error NotionError :> es) =>
UUID ->
Eff es Text
demo pid = do
page <- NE.retrievePage pid
pure (Text.pack (show page))
main :: IO ()
main = do
token <- Text.pack <$> Env.getEnv "NOTION_TOKEN"
env <- getClientEnv "https://api.notion.com/v1"
let methods = makeMethods env token
result <-
runEff
. runErrorNoCallStack @NotionError
. NE.runNotion methods
$ demo (UUID "00000000-0000-0000-0000-000000000000")
print result
What you get
One smart constructor per field of Notion.V1.Methods. The
argument types and order match the underlying field, so an
existing call like
page <- retrievePage methods pid
becomes
page <- NE.retrievePage pid
with no other change to the call site.
Versioning
The 0.y.z series tracks notion-client 0.7.x. A major version
bump is planned if notion-client changes the shape of Methods
in a way that forces a constructor rename.