Skip to content
Merged
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
37 changes: 37 additions & 0 deletions .beans/beans-rvfe--extract-core-graphql-into-pkgbeangraph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
# beans-rvfe
title: Extract core GraphQL into pkg/beangraph
status: completed
type: task
priority: normal
created_at: 2026-03-21T08:11:51Z
updated_at: 2026-03-21T08:24:36Z
---

Split the GraphQL layer: core bean CRUD into pkg/beangraph/ (public), UI-specific resolvers stay in internal/graph/. Move model types to pkg/beangraph/model/. CLI commands switch to using CoreResolver directly, dropping internal/graph dependency.

## Tasks

- [x] Create `pkg/beangraph/` package with `CoreResolver` struct
- [x] Move model types from `internal/graph/model/` to `pkg/beangraph/model/`
- [x] Update gqlgen.yml to generate models in new location
- [x] Move core bean query resolvers to `pkg/beangraph/queries.go`
- [x] Move core bean mutation resolvers to `pkg/beangraph/mutations.go`
- [x] Move bean field resolvers to `pkg/beangraph/bean_fields.go`
- [x] Move filter logic to `pkg/beangraph/filters.go`
- [x] Move ETag/validation helpers to `pkg/beangraph/resolver.go`
- [x] Update `internal/graph/` to embed `CoreResolver` and delegate
- [x] Update CLI commands to use `beangraph.CoreResolver` directly
- [x] Run codegen, verify build compiles
- [x] Run tests

## Summary of Changes

- Created `pkg/beangraph/` package with `CoreResolver` struct containing all core bean CRUD operations
- Moved model types from `internal/graph/model/` to `pkg/beangraph/model/` (updated gqlgen.yml)
- Extracted core resolvers: queries (Bean, Beans, ProjectName, MainBranch), mutations (CreateBean, UpdateBean, DeleteBean, SetParent, Add/RemoveBlocking, Add/RemoveBlockedBy, ArchiveBean), bean field resolvers, filter logic, and ETag/validation helpers
- `internal/graph/Resolver` now embeds `*beangraph.CoreResolver` and delegates core operations
- CLI commands (`create`, `list`, `show`, `update`, `delete`, `roadmap`) now use `beangraph.CoreResolver` directly, removing their dependency on `internal/graph`
- TUI also migrated to use `beangraph.CoreResolver` directly
- Only `graphql.go` and `serve.go` still import `internal/graph` (expected — they need the full gqlgen schema)
- All 16 test packages pass, codegen works
2 changes: 1 addition & 1 deletion gqlgen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ exec:
package: graph

model:
filename: internal/graph/model/models_gen.go
filename: pkg/beangraph/model/models_gen.go
package: model

resolver:
Expand Down
10 changes: 5 additions & 5 deletions internal/commands/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"strings"

"github.com/hmans/beans/pkg/config"
"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/internal/graph/model"
"github.com/hmans/beans/pkg/beangraph"
"github.com/hmans/beans/pkg/beangraph/model"
"github.com/hmans/beans/internal/output"
"github.com/hmans/beans/internal/ui"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -98,9 +98,9 @@ var createCmd = &cobra.Command{
input.Prefix = &createPrefix
}

// Create via GraphQL mutation
resolver := &graph.Resolver{Core: core}
b, err := resolver.Mutation().CreateBean(context.Background(), input)
// Create via core resolver
resolver := &beangraph.CoreResolver{Core: core}
b, err := resolver.CreateBean(context.Background(), input)
if err != nil {
return cmdError(createJSON, output.ErrFileError, "failed to create bean: %v", err)
}
Expand Down
8 changes: 4 additions & 4 deletions internal/commands/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

"github.com/hmans/beans/pkg/bean"
"github.com/hmans/beans/pkg/beancore"
"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/pkg/beangraph"
"github.com/hmans/beans/internal/output"
"github.com/spf13/cobra"
)
Expand All @@ -36,12 +36,12 @@ warned and those references will be removed after confirmation. Use -f to skip a
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
resolver := &graph.Resolver{Core: core}
resolver := &beangraph.CoreResolver{Core: core}

// Collect all beans and their incoming links upfront (validate before deleting)
var targets []beanWithLinks
for _, id := range args {
b, err := resolver.Query().Bean(ctx, id)
b, err := resolver.Bean(ctx, id)
if err != nil {
return cmdError(deleteJSON, output.ErrNotFound, "failed to find bean: %v", err)
}
Expand All @@ -66,7 +66,7 @@ warned and those references will be removed after confirmation. Use -f to skip a
var deleted []*bean.Bean
var totalLinksRemoved int
for _, target := range targets {
_, err := resolver.Mutation().DeleteBean(ctx, target.bean.ID)
_, err := resolver.DeleteBean(ctx, target.bean.ID)
if err != nil {
return cmdError(deleteJSON, output.ErrFileError, "failed to delete bean %s: %v", target.bean.ID, err)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/commands/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/vektah/gqlparser/v2/formatter"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/pkg/beangraph"
)

var (
Expand Down Expand Up @@ -138,7 +139,7 @@ func readFromStdin() (string, error) {
// On error, it returns an error so the CLI can handle it appropriately.
func executeQuery(query string, variables map[string]any, operationName string) ([]byte, error) {
es := graph.NewExecutableSchema(graph.Config{
Resolvers: &graph.Resolver{Core: core},
Resolvers: &graph.Resolver{CoreResolver: &beangraph.CoreResolver{Core: core}},
})

exec := executor.New(es)
Expand Down Expand Up @@ -191,7 +192,7 @@ func printSchema() error {
// This is exported so it can be used by other commands like prompt.
func GetGraphQLSchema() string {
es := graph.NewExecutableSchema(graph.Config{
Resolvers: &graph.Resolver{Core: core},
Resolvers: &graph.Resolver{CoreResolver: &beangraph.CoreResolver{Core: core}},
})

var buf bytes.Buffer
Expand Down
12 changes: 6 additions & 6 deletions internal/commands/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"sort"

"github.com/hmans/beans/pkg/bean"
"github.com/hmans/beans/pkg/beangraph"
"github.com/hmans/beans/pkg/beangraph/model"
"github.com/hmans/beans/pkg/config"
"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/internal/graph/model"
"github.com/hmans/beans/internal/output"
"github.com/hmans/beans/internal/ui"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -111,9 +111,9 @@ Search Syntax (--search/-S):
filter.ExcludeImplicitTerminal = &excludeImplicitTerminal
}

// Execute query via GraphQL resolver
resolver := &graph.Resolver{Core: core}
beans, err := resolver.Query().Beans(context.Background(), filter)
// Execute query via core resolver
resolver := &beangraph.CoreResolver{Core: core}
beans, err := resolver.Beans(context.Background(), filter)
if err != nil {
return fmt.Errorf("querying beans: %w", err)
}
Expand Down Expand Up @@ -141,7 +141,7 @@ Search Syntax (--search/-S):

// Default: tree view
// We need all beans to find ancestors for context
allBeans, err := resolver.Query().Beans(context.Background(), nil)
allBeans, err := resolver.Beans(context.Background(), nil)
if err != nil {
return fmt.Errorf("querying all beans for tree: %w", err)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/commands/roadmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"text/template"

"github.com/hmans/beans/pkg/bean"
"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/pkg/beangraph"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -60,8 +60,8 @@ var roadmapCmd = &cobra.Command{
Short: "Generate a Markdown roadmap from milestones and epics",
RunE: func(cmd *cobra.Command, args []string) error {
// Query all beans via GraphQL resolver
resolver := &graph.Resolver{Core: core}
allBeans, err := resolver.Query().Beans(context.Background(), nil)
resolver := &beangraph.CoreResolver{Core: core}
allBeans, err := resolver.Beans(context.Background(), nil)
if err != nil {
return fmt.Errorf("querying beans: %w", err)
}
Expand Down
15 changes: 8 additions & 7 deletions internal/commands/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/hmans/beans/internal/terminal"
"github.com/hmans/beans/internal/web"
"github.com/hmans/beans/internal/worktree"
"github.com/hmans/beans/pkg/beangraph"
"github.com/hmans/beans/pkg/config"
"github.com/hmans/beans/pkg/forge"
)
Expand Down Expand Up @@ -331,13 +332,13 @@ func runServer(port int, origins []string) error {
// Create GraphQL server with explicit transports
es := graph.NewExecutableSchema(graph.Config{
Resolvers: &graph.Resolver{
Core: core,
WorktreeMgr: wtManager,
AgentMgr: agentMgr,
TerminalMgr: termMgr,
PortAlloc: portAlloc,
Forge: forgeProvider,
ProjectRoot: projectRoot,
CoreResolver: &beangraph.CoreResolver{Core: core},
WorktreeMgr: wtManager,
AgentMgr: agentMgr,
TerminalMgr: termMgr,
PortAlloc: portAlloc,
Forge: forgeProvider,
ProjectRoot: projectRoot,
},
})
gqlHandler := handler.New(es)
Expand Down
6 changes: 3 additions & 3 deletions internal/commands/show.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/lipgloss"
"github.com/hmans/beans/pkg/bean"
"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/pkg/beangraph"
"github.com/hmans/beans/internal/output"
"github.com/hmans/beans/internal/ui"
"github.com/spf13/cobra"
Expand All @@ -27,12 +27,12 @@ var showCmd = &cobra.Command{
Long: `Displays the full contents of one or more beans, including front matter and body.`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
resolver := &graph.Resolver{Core: core}
resolver := &beangraph.CoreResolver{Core: core}

// Collect all beans
var beans []*bean.Bean
for _, id := range args {
b, err := resolver.Query().Bean(context.Background(), id)
b, err := resolver.Bean(context.Background(), id)
if err != nil {
if showJSON {
return output.Error(output.ErrNotFound, err.Error())
Expand Down
12 changes: 6 additions & 6 deletions internal/commands/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import (
"github.com/hmans/beans/pkg/bean"
"github.com/hmans/beans/pkg/beancore"
"github.com/hmans/beans/pkg/config"
"github.com/hmans/beans/internal/graph"
"github.com/hmans/beans/internal/graph/model"
"github.com/hmans/beans/pkg/beangraph"
"github.com/hmans/beans/pkg/beangraph/model"
"github.com/hmans/beans/internal/output"
"github.com/hmans/beans/internal/ui"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -46,10 +46,10 @@ var updateCmd = &cobra.Command{
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
resolver := &graph.Resolver{Core: core}
resolver := &beangraph.CoreResolver{Core: core}

// Find the bean
b, err := resolver.Query().Bean(ctx, args[0])
b, err := resolver.Bean(ctx, args[0])
if err != nil {
return cmdError(updateJSON, output.ErrNotFound, "failed to find bean: %v", err)
}
Expand All @@ -62,7 +62,7 @@ var updateCmd = &cobra.Command{
return cmdError(updateJSON, output.ErrNotFound, "bean not found: %s", args[0])
}
// Re-query to get the model.Bean
b, err = resolver.Query().Bean(ctx, unarchived.ID)
b, err = resolver.Bean(ctx, unarchived.ID)
if err != nil || b == nil {
return cmdError(updateJSON, output.ErrNotFound, "bean not found: %s", args[0])
}
Expand Down Expand Up @@ -93,7 +93,7 @@ var updateCmd = &cobra.Command{
// Apply all updates atomically via single UpdateBean mutation
// This includes field updates, body modifications, and relationship changes
if hasFieldUpdates(input) {
b, err = resolver.Mutation().UpdateBean(ctx, b.ID, input)
b, err = resolver.UpdateBean(ctx, b.ID, input)
if err != nil {
return mutationError(updateJSON, err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/graph/agent_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"

"github.com/hmans/beans/internal/agent"
"github.com/hmans/beans/internal/graph/model"
"github.com/hmans/beans/pkg/beangraph/model"
"github.com/hmans/beans/pkg/forge"
)

Expand Down
Loading
Loading