Compare commits

...

1 Commits

Author SHA1 Message Date
Vincent Koc
446cccad81
perf(markdown): preload export path metadata
Some checks failed
Validation / validate (push) Has been cancelled
2026-04-27 12:30:44 -07:00
4 changed files with 190 additions and 14 deletions

View File

@ -39,10 +39,14 @@ func (e Exporter) Export(ctx context.Context) (Summary, error) {
if err != nil {
return Summary{}, err
}
paths, err := newPathResolver(ctx, e.Store)
if err != nil {
return Summary{}, err
}
var s Summary
keep := map[string]bool{}
for _, page := range pages {
path, err := e.writePage(ctx, page)
path, err := e.writePage(ctx, paths, page)
if err != nil {
return s, err
}
@ -56,19 +60,10 @@ func (e Exporter) Export(ctx context.Context) (Summary, error) {
return s, nil
}
func (e Exporter) writePage(ctx context.Context, page store.Page) (string, error) {
spaceName, err := e.Store.SpaceName(ctx, page.SpaceID)
if err != nil {
return "", err
}
teamID, err := e.Store.PageTeamID(ctx, page)
if err != nil {
return "", err
}
teamName, err := e.Store.TeamName(ctx, teamID)
if err != nil {
return "", err
}
func (e Exporter) writePage(ctx context.Context, paths pathResolver, page store.Page) (string, error) {
spaceName := paths.spaceName(page.SpaceID)
teamID := paths.pageTeamID(page)
teamName := paths.teamName(teamID)
blocks, err := e.Store.PageBlocks(ctx, page.ID)
if err != nil {
return "", err
@ -112,6 +107,80 @@ func (e Exporter) writePage(ctx context.Context, page store.Page) (string, error
return path, os.WriteFile(path, []byte(out), 0o644)
}
type pathResolver struct {
spaces map[string]string
teams map[string]string
blocks map[string]store.ParentRef
collections map[string]store.ParentRef
}
func newPathResolver(ctx context.Context, st *store.Store) (pathResolver, error) {
spaces, err := st.SpaceNames(ctx)
if err != nil {
return pathResolver{}, err
}
teams, err := st.TeamNames(ctx)
if err != nil {
return pathResolver{}, err
}
blocks, err := st.BlockParents(ctx)
if err != nil {
return pathResolver{}, err
}
collections, err := st.CollectionParents(ctx)
if err != nil {
return pathResolver{}, err
}
return pathResolver{spaces: spaces, teams: teams, blocks: blocks, collections: collections}, nil
}
func (r pathResolver) spaceName(id string) string {
if id == "" {
return "default"
}
if name := r.spaces[id]; name != "" {
return name
}
return "space-" + notiontext.ShortID(id)
}
func (r pathResolver) teamName(id string) string {
if id == "" {
return ""
}
if name := r.teams[id]; name != "" {
return name
}
return "team-" + notiontext.ShortID(id)
}
func (r pathResolver) pageTeamID(page store.Page) string {
return r.resolveTeamID(page.ParentTable, page.ParentID, page.CollectionID, map[string]bool{page.ID: true})
}
func (r pathResolver) resolveTeamID(table, id, collectionID string, seen map[string]bool) string {
if table == "team" {
return id
}
if table == "collection" && id == "" {
id = collectionID
}
if id == "" || seen[table+":"+id] {
return ""
}
seen[table+":"+id] = true
switch table {
case "block":
parent := r.blocks[id]
return r.resolveTeamID(parent.Table, parent.ID, "", seen)
case "collection", "database", "data_source":
parent := r.collections[id]
return r.resolveTeamID(parent.Table, parent.ID, "", seen)
default:
return ""
}
}
func writeFrontMatter(b *strings.Builder, page store.Page, spaceName, teamID, teamName string) {
b.WriteString("---\n")
writeKV(b, "id", page.ID)

View File

@ -142,6 +142,38 @@ func TestExporterUsesWorkspaceAndTeamspacePath(t *testing.T) {
}
}
func TestExporterResolvesTeamspaceThroughCollectionParent(t *testing.T) {
ctx := context.Background()
st, err := store.Open(filepath.Join(t.TempDir(), "notcrawl.db"))
if err != nil {
t.Fatal(err)
}
defer st.Close()
now := store.NowMS()
if err := st.UpsertSpace(ctx, store.Space{ID: "space1", Name: "Acme Org", Source: "test", SyncedAt: now}); err != nil {
t.Fatal(err)
}
if err := st.UpsertTeam(ctx, store.Team{ID: "team1", SpaceID: "space1", Name: "Research Lab", Source: "test", SyncedAt: now}); err != nil {
t.Fatal(err)
}
if err := st.UpsertCollection(ctx, store.Collection{ID: "collection1", SpaceID: "space1", ParentID: "team1", ParentTable: "team", Name: "Roadmap", Source: "test", SyncedAt: now}); err != nil {
t.Fatal(err)
}
if err := st.UpsertPage(ctx, store.Page{ID: "page1", SpaceID: "space1", ParentID: "collection1", ParentTable: "collection", CollectionID: "collection1", Title: "Row", Alive: true, Source: "test", SyncedAt: now}); err != nil {
t.Fatal(err)
}
dir := t.TempDir()
s, err := Exporter{Store: st, Dir: dir}.Export(ctx)
if err != nil {
t.Fatal(err)
}
want := filepath.Join(dir, "acme-org", "research-lab", "row-page1.md")
if len(s.Files) != 1 || s.Files[0] != want {
t.Fatalf("unexpected export path: %+v, want %s", s.Files, want)
}
}
func TestExporterUsesReadableMissingSpaceFallback(t *testing.T) {
ctx := context.Background()
st, err := store.Open(filepath.Join(t.TempDir(), "notcrawl.db"))

View File

@ -119,6 +119,76 @@ func (s *Store) PageComments(ctx context.Context, pageID string) ([]Comment, err
return comments, rows.Err()
}
func (s *Store) SpaceNames(ctx context.Context) (map[string]string, error) {
rows, err := s.queryContext(ctx, `select id, name from spaces`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]string{}
for rows.Next() {
var id, name string
if err := rows.Scan(&id, &name); err != nil {
return nil, err
}
out[id] = name
}
return out, rows.Err()
}
func (s *Store) TeamNames(ctx context.Context) (map[string]string, error) {
rows, err := s.queryContext(ctx, `select id, name from teams`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]string{}
for rows.Next() {
var id, name string
if err := rows.Scan(&id, &name); err != nil {
return nil, err
}
out[id] = name
}
return out, rows.Err()
}
func (s *Store) BlockParents(ctx context.Context) (map[string]ParentRef, error) {
rows, err := s.queryContext(ctx, `select id, parent_id, parent_table from blocks`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]ParentRef{}
for rows.Next() {
var id string
var parentID, parentTable sql.NullString
if err := rows.Scan(&id, &parentID, &parentTable); err != nil {
return nil, err
}
out[id] = ParentRef{ID: parentID.String, Table: parentTable.String}
}
return out, rows.Err()
}
func (s *Store) CollectionParents(ctx context.Context) (map[string]ParentRef, error) {
rows, err := s.queryContext(ctx, `select id, parent_id, parent_table from collections`)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]ParentRef{}
for rows.Next() {
var id string
var parentID, parentTable sql.NullString
if err := rows.Scan(&id, &parentID, &parentTable); err != nil {
return nil, err
}
out[id] = ParentRef{ID: parentID.String, Table: parentTable.String}
}
return out, rows.Err()
}
func (s *Store) SpaceName(ctx context.Context, id string) (string, error) {
if id == "" {
return "default", nil

View File

@ -105,6 +105,11 @@ type RawRecord struct {
SyncedAt int64
}
type ParentRef struct {
ID string
Table string
}
type SearchResult struct {
Kind string
ID string