Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 63 additions & 6 deletions cmd/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import (
)

type PageCmd struct {
List PageListCmd `cmd:"" help:"List pages"`
View PageViewCmd `cmd:"" help:"View a page"`
Create PageCreateCmd `cmd:"" help:"Create a page"`
Upload PageUploadCmd `cmd:"" help:"Upload a markdown file as a page"`
Sync PageSyncCmd `cmd:"" help:"Sync a markdown file to a page (create or update)"`
Edit PageEditCmd `cmd:"" help:"Edit a page"`
List PageListCmd `cmd:"" help:"List pages"`
View PageViewCmd `cmd:"" help:"View a page"`
Create PageCreateCmd `cmd:"" help:"Create a page"`
Upload PageUploadCmd `cmd:"" help:"Upload a markdown file as a page"`
Sync PageSyncCmd `cmd:"" help:"Sync a markdown file to a page (create or update)"`
Edit PageEditCmd `cmd:"" help:"Edit a page"`
Archive PageArchiveCmd `cmd:"" help:"Archive a page"`
}

type PageListCmd struct {
Expand Down Expand Up @@ -369,6 +370,62 @@ func runPageEdit(ctx *Context, page, replace, find, replaceWith, appendText stri
return nil
}

type PageArchiveCmd struct {
Page string `arg:"" help:"Page URL, name, or ID"`
}

func (c *PageArchiveCmd) Run(ctx *Context) error {
return runPageArchive(ctx, c.Page)
}

func runPageArchive(ctx *Context, page string) error {
bgCtx := context.Background()

ref := cli.ParsePageRef(page)
pageID := ref.ID
switch ref.Kind {
case cli.RefName:
client, err := cli.RequireClient()
if err != nil {
return err
}
defer func() { _ = client.Close() }()

resolved, err := cli.ResolvePageID(bgCtx, client, page)
if err != nil {
output.PrintError(err)
return err
}
pageID = resolved
case cli.RefID:
pageID = ref.ID
case cli.RefURL:
if extractedID, ok := cli.ExtractNotionUUID(page); ok {
pageID = extractedID
break
}
return &output.UserError{Message: fmt.Sprintf("could not extract page ID from URL: %s\nUse the page ID directly instead.", page)}
}

if err := archivePageViaOfficialAPI(bgCtx, pageID); err != nil {
output.PrintError(err)
return err
}

output.PrintSuccess("Page archived")
return nil
}

func archivePageViaOfficialAPI(ctx context.Context, pageID string) error {
client, err := cli.RequireOfficialAPIClient()
if err != nil {
return err
}
return client.PatchPage(ctx, pageID, map[string]any{
"archived": true,
})
}

type PageSyncCmd struct {
File string `arg:"" help:"Markdown file to sync" type:"existingfile"`
Title string `help:"Page title (default: filename or first heading)" short:"t"`
Expand Down
91 changes: 91 additions & 0 deletions cmd/page_archive_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cmd

import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestRunPageArchiveUsesOfficialAPI(t *testing.T) {
pageID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"

var gotMethod string
var gotPath string
var gotAuth string
var gotBody map[string]any

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotMethod = r.Method
gotPath = r.URL.Path
gotAuth = r.Header.Get("Authorization")

defer func() { _ = r.Body.Close() }()
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
t.Fatalf("decode request body: %v", err)
}

w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"` + pageID + `","object":"page","archived":true}`))
}))
defer srv.Close()

t.Setenv("HOME", t.TempDir())
t.Setenv("NOTION_API_BASE_URL", srv.URL+"/v1")
t.Setenv("NOTION_API_TOKEN", "test-token")

if err := runPageArchive(&Context{}, pageID); err != nil {
t.Fatalf("runPageArchive: %v", err)
}

if gotMethod != http.MethodPatch {
t.Fatalf("method mismatch: got %s", gotMethod)
}
if gotPath != "/v1/pages/"+pageID {
t.Fatalf("path mismatch: got %s", gotPath)
}
if gotAuth != "Bearer test-token" {
t.Fatalf("auth mismatch: got %q", gotAuth)
}
if gotBody["archived"] != true {
t.Fatalf("archived payload mismatch: %#v", gotBody["archived"])
}
}

func TestRunPageArchiveSupportsURLInputWithEmbeddedID(t *testing.T) {
pageID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
pageURL := "https://www.notion.so/My-Page-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

var gotPath string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"` + pageID + `","object":"page","archived":true}`))
}))
defer srv.Close()

t.Setenv("HOME", t.TempDir())
t.Setenv("NOTION_API_BASE_URL", srv.URL+"/v1")
t.Setenv("NOTION_API_TOKEN", "test-token")

if err := runPageArchive(&Context{}, pageURL); err != nil {
t.Fatalf("runPageArchive: %v", err)
}
if gotPath != "/v1/pages/"+pageID {
t.Fatalf("path mismatch: got %s", gotPath)
}
}

func TestRunPageArchiveRequiresOfficialAPIToken(t *testing.T) {
t.Setenv("HOME", t.TempDir())
t.Setenv("NOTION_API_BASE_URL", "http://127.0.0.1:65535/v1")

err := runPageArchive(&Context{}, "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
if err == nil {
t.Fatal("expected missing official API token error")
}
if !strings.Contains(err.Error(), "official API token is required") {
t.Fatalf("unexpected error: %v", err)
}
}