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.
The umbrella module exposes the common operations. For the complete surface,
including filtered page requests, background operations, meeting notes and
comment retrieval/update/deletion, import the effect and interpreter directly:
import Notion.V1.Effectful.Effect qualified as NE
import Notion.V1.Effectful.Interpreter (runNotion)
Use NE.createPageFiltered, NE.createPageAsync, NE.retrieveAsyncTask, and
the other effect operations with the unqualified runNotion interpreter.
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 and reactive OAuth renewal happen inside Methods, before an error
reaches runNotion.
OAuth installations
Build Methods with core makeMethodsWithTokenSource and pass it to runNotion;
existing effect operations need no changes. See the core
OAuth walkthrough for browser
login and token-store setup.
OAuthError, ReauthorizationRequired, TokenPersistenceError and
RefreshOutcomeUnknown propagate as IO exceptions. Handle them at the application
boundary outside runEff; Error NotionError catches only final Notion API errors.
For a persistence failure, call retryTokenPersistence on the same manager to
save its retained pair without another refresh. Reauthorization or an uncertain
remote outcome requires a new login and manager. Use one manager per installation;
multiple processes must coordinate the entire load/refresh/save sequence.
Minimum viable usage
Use GHC2024 with OverloadedStrings for this example.
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
Use compatible versions of both packages:
notion-client-effectful |
Required notion-client |
| 0.4.x |
>=0.10 && <0.11 |
| 0.3.x |
>=0.9 && <0.10 |
| 0.2.x |
>=0.8 && <0.9 |
The prepared release is notion-client-effectful-0.4.0.0 with
notion-client-0.10.0.0. The companion's core bound moves with each breaking core
release, even when Methods fields are unchanged. See the
changelog for migration details and the repository's
release preparation
for validation.