Skip to content

feat(api): add core API client primitives#14

Open
0xble wants to merge 3 commits into
mainfrom
feat/api-client
Open

feat(api): add core API client primitives#14
0xble wants to merge 3 commits into
mainfrom
feat/api-client

Conversation

@0xble
Copy link
Copy Markdown
Owner

@0xble 0xble commented Feb 28, 2026

  • adds official Notion API client primitives\n- adds config support for API base URL, version, and token\n- adds a CLI helper to construct the official API client from config/env\n- intentionally excludes icon/archive/image/property feature logic so this can be the foundation PR

Summary by CodeRabbit

  • New Features
    • Added Notion API authentication with configuration file and environment variable support for base URL, API version, and access tokens.
    • Enabled updating Notion pages with support for archiving, custom icons, covers, and properties.
    • Implemented automatic retry handling for rate-limited requests with detailed error reporting.

Introduce a dedicated official Notion REST API client package and config loader scaffolding.

Adds API base URL, Notion-Version, and token config with env overrides, plus tests for request behavior and config normalization.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 18, 2026

📝 Walkthrough

Walkthrough

A new Notion API HTTP client is introduced with structured request/response handling, configuration management system, and CLI integration. The client supports PATCH operations with error handling and 429 throttling retry logic. Configuration is loaded from a JSON file with environment variable overrides.

Changes

Cohort / File(s) Summary
API Client
internal/api/client.go, internal/api/client_test.go
Introduces Client struct with PatchPage method for PATCH operations, APIError for error context (status code, error code, message, request ID), and PageUpdate with validation. Implements request marshaling, response decoding, header setup, minimal 429 retry (honoring Retry-After), and error parsing via parseAPIError. Tests verify token validation, request/response correctness, URL escaping, typed error handling, 429 retry behavior, and empty update rejection.
Configuration System
internal/config/config.go, internal/config/config_test.go
Introduces Config and APIConfig structs with file-based loading from ~/.config/notion-cli/config.json. Load() applies environment variable overrides (NOTION_API_BASE_URL, NOTION_API_NOTION_VERSION, NOTION_API_TOKEN), normalizes paths (trailing slash removal, default fallbacks), and returns defaults if file absent. Tests confirm environment variable application, path resolution, normalization, and default population.
CLI Integration
internal/cli/official_api.go
Adds RequireOfficialAPIClient() helper that loads configuration and constructs an API client, returning typed errors for config/client creation failures with actionable messages.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hops through config files with glee,
HTTP requests dance so free,
When 429 slows us down, we retry with care,
Notion's API now within our snare!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(api): add core API client primitives' directly and accurately describes the main changeset, which introduces a new Notion API HTTP client, configuration management, and CLI support for constructing the API client.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/api-client

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
internal/config/config_test.go (1)

37-47: Consider adding coverage for Load() integration scenarios.

The unit tests cover the individual helper functions well, but there's no direct test for Load() that exercises file reading with JSON parsing (valid/invalid JSON, partial config, etc.). This could help catch integration issues.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/config/config_test.go` around lines 37 - 47, Add integration tests
for Load() that create a real config file at the path returned by Path() (use
t.Setenv("HOME", ...) or a temp dir and ensure Path() points there), then call
Load() and assert behavior: one test with valid JSON to verify all fields are
parsed into the Config struct, one test with partial JSON to verify
defaults/zero-values are handled, and one test with invalid JSON to assert
Load() returns a parsing error. Use os.WriteFile to create the file before
calling Load() and remove/cleanup after; reference the Path() and Load()
functions and the Config type when asserting expected output.
internal/api/client.go (1)

156-171: Consider capping the Retry-After duration to prevent excessive waits.

The current implementation honors any Retry-After value returned by the server. A malicious or misconfigured server could return a very large value (e.g., hours), causing the client to block indefinitely even with a context timeout (since the select only checks ctx.Done() at wait start). Consider capping to a reasonable maximum (e.g., 60 seconds).

Suggested improvement
 	if resp.StatusCode == http.StatusTooManyRequests {
 		retryAfter := parseRetryAfter(resp.Header.Get("Retry-After"))
 		_ = resp.Body.Close()
+		const maxRetryWait = 60 * time.Second
+		if retryAfter > maxRetryWait {
+			retryAfter = maxRetryWait
+		}
 		if retryAfter > 0 {
 			select {
 			case <-ctx.Done():
 				return ctx.Err()
 			case <-time.After(retryAfter):
 			}
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/api/client.go` around lines 156 - 171, The 429 retry wait should be
capped to avoid very long server-provided Retry-After values; in the block that
checks resp.StatusCode == http.StatusTooManyRequests (using parseRetryAfter and
before calling c.sendOnce), compute a capped duration (e.g., maxRetry := 60 *
time.Second and wait := min(parseRetryAfter(...), maxRetry)), use that capped
wait in the select that checks ctx.Done() and time.After(wait), and then proceed
to call c.sendOnce; keep the existing resp.Body.Close() and error handling but
replace the raw retryAfter usage with the capped value to prevent excessive
waits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/api/client.go`:
- Around line 156-171: The 429 retry wait should be capped to avoid very long
server-provided Retry-After values; in the block that checks resp.StatusCode ==
http.StatusTooManyRequests (using parseRetryAfter and before calling
c.sendOnce), compute a capped duration (e.g., maxRetry := 60 * time.Second and
wait := min(parseRetryAfter(...), maxRetry)), use that capped wait in the select
that checks ctx.Done() and time.After(wait), and then proceed to call
c.sendOnce; keep the existing resp.Body.Close() and error handling but replace
the raw retryAfter usage with the capped value to prevent excessive waits.

In `@internal/config/config_test.go`:
- Around line 37-47: Add integration tests for Load() that create a real config
file at the path returned by Path() (use t.Setenv("HOME", ...) or a temp dir and
ensure Path() points there), then call Load() and assert behavior: one test with
valid JSON to verify all fields are parsed into the Config struct, one test with
partial JSON to verify defaults/zero-values are handled, and one test with
invalid JSON to assert Load() returns a parsing error. Use os.WriteFile to
create the file before calling Load() and remove/cleanup after; reference the
Path() and Load() functions and the Config type when asserting expected output.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ef107418-44e9-4bc9-9682-95a26f7d28ee

📥 Commits

Reviewing files that changed from the base of the PR and between bec90a0 and f6d6333.

📒 Files selected for processing (5)
  • internal/api/client.go
  • internal/api/client_test.go
  • internal/cli/official_api.go
  • internal/config/config.go
  • internal/config/config_test.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant