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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,12 @@ go run cmd/graphql-schema-picker/main.go \
pick \
--sdl-file examples/hasura.sdl.graphqls \
--definitions Aircrafts
```
```

## Similar Tools

- https://github.com/n1ru4l/graphql-public-schema-filter
- https://github.com/kesne/graphql-schema-subset
- https://github.com/xometry/graphql-code-generator-subset-plugin
- https://the-guild.dev/graphql/tools/docs/api/classes/wrap_src.pruneschema
- https://pothos-graphql.dev/docs/plugins/sub-graph
112 changes: 66 additions & 46 deletions internal/cli/graph.go
Original file line number Diff line number Diff line change
@@ -1,39 +1,101 @@
package cli

import (
"errors"
"github.com/charmbracelet/log"
"github.com/dominikbraun/graph"
"github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/kinds"
"strings"
)

type Vertex struct {
Name string
Node ast.Node
}

type Describer interface {
GetDescription() *ast.StringValue
}

func sanitizeComment(d Describer) string {
desc := d.GetDescription()
if desc == nil {
return ""
}

return strings.ReplaceAll(desc.Value, `"`, `'`)
}

func NewVertex(node ast.Node) Vertex {
var name string
switch node.GetKind() {
case kinds.ScalarDefinition:
obj := node.(*ast.ScalarDefinition)
name = obj.GetName().Value

// Sanitize description (e.g., remove double-quotes)
if obj.Description != nil {
obj.Description = &ast.StringValue{
Kind: kinds.StringValue,
Value: sanitizeComment(obj),
}
}
case kinds.InterfaceDefinition:
obj := node.(*ast.InterfaceDefinition)
name = obj.GetName().Value

// Sanitize description (e.g., remove double-quotes)
if obj.Description != nil {
obj.Description = &ast.StringValue{
Kind: kinds.StringValue,
Value: sanitizeComment(obj),
}
}
case kinds.UnionDefinition:
obj := node.(*ast.UnionDefinition)
name = obj.GetName().Value

// Sanitize description (e.g., remove double-quotes)
if obj.Description != nil {
obj.Description = &ast.StringValue{
Kind: kinds.StringValue,
Value: sanitizeComment(obj),
}
}
case kinds.EnumDefinition:
obj := node.(*ast.EnumDefinition)
name = obj.GetName().Value

// Sanitize description (e.g., remove double-quotes)
if obj.Description != nil {
obj.Description = &ast.StringValue{
Kind: kinds.StringValue,
Value: sanitizeComment(obj),
}
}
case kinds.InputObjectDefinition:
obj := node.(*ast.InputObjectDefinition)
name = obj.GetName().Value

// Sanitize description (e.g., remove double-quotes)
if obj.Description != nil {
obj.Description = &ast.StringValue{
Kind: kinds.StringValue,
Value: sanitizeComment(obj),
}
}

case kinds.ObjectDefinition:
obj := node.(*ast.ObjectDefinition)
name = obj.GetName().Value

// Sanitize description (e.g., remove double-quotes)
if obj.Description != nil {
obj.Description = &ast.StringValue{
Kind: kinds.StringValue,
Value: sanitizeComment(obj),
}
}
default:
panic("NewVertex: unsupported node kind: " + node.GetKind())
}
Expand All @@ -60,7 +122,7 @@ func buildPrunedGraph(doc *ast.Document) graph.Graph[string, Vertex] {
loadTopLevelDefinitions(g, doc)

// Build edges between vertices.
buildEdges(g, doc)
buildEdges(g)

// Prunes any vertices that don't appear in any edges
return prune(g)
Expand All @@ -82,57 +144,15 @@ func loadTopLevelDefinitions(g graph.Graph[string, Vertex], doc *ast.Document) {
case kinds.InputObjectDefinition:
v := NewVertex(d)
_ = g.AddVertex(v)
log.Debugf("Adding vertex for definition %d (%s) -- %s", i, d.GetKind(), v.Name)
log.Debugf("Adding vertex for definition %d (%s) -- %s",
i, d.GetKind(), v.Name)

default:
log.Warnf("Ignoring definition %d (%s)", i, d.GetKind())
}
}
}

func buildEdges(g graph.Graph[string, Vertex], doc *ast.Document) {
for _, desired := range desiredDefinitions {
v, err := g.Vertex(desired)
if err != nil {
if errors.Is(err, graph.ErrVertexNotFound) {
log.Errorf("unable to find definition for: %s", desired)
} else {
log.Fatal("unable to read vertex", "err", err)
}
}

switch v.Node.GetKind() {
// TODO support input objects, input values, interfaces, and unions... otherwise we're missing things like AircraftsBoolExp
case kinds.ObjectDefinition:
obj := v.Node.(*ast.ObjectDefinition)
fields := obj.Fields
// TODO iterate through node's fields
for _, f := range fields {

// Is the field a primitive scalar (e.g., Int, String)?
// If so, we can skip it, as it's natively a part of any
// GraphQL schema.
rootType := getRootTypeNameHelper(f.Type, 0)
if isBasicType(rootType) {
continue
}

log.Debug("Found field in object",
"object", obj.Name.Value,
"name", f.Name.Value,
"type", rootType,
)

_ = g.AddEdge(obj.Name.Value, rootType)

// TODO Fields also consist of their arguments, which themselves
// may be non-primitive dependencies.
//litter.Dump(f.Arguments)
}
}
}
}

func prune(in graph.Graph[string, Vertex]) graph.Graph[string, Vertex] {
m, err := in.AdjacencyMap()
if err != nil {
Expand Down
110 changes: 110 additions & 0 deletions internal/cli/graph_edges.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package cli

import (
"errors"
"github.com/charmbracelet/log"
"github.com/dominikbraun/graph"
"github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/kinds"
)

func buildEdges(g graph.Graph[string, Vertex]) {
for _, desired := range desiredDefinitions {
v, err := g.Vertex(desired)
if err != nil {
if errors.Is(err, graph.ErrVertexNotFound) {
log.Errorf("unable to find definition for: %s", desired)
} else {
log.Fatal("unable to read vertex", "err", err)
}
}

switch v.Node.GetKind() {
case kinds.ObjectDefinition:
buildEdgesForObject(v, g)
case kinds.InterfaceDefinition:
buildEdgesForInterface(v, g)
case kinds.UnionDefinition:
buildEdgesForUnion(v, g)
case kinds.InputObjectDefinition:
buildEdgesForInputObject(v, g)
default:
log.Warnf("Ignoring dependencies for %s node", v.Node.GetKind())
}
}
}

func buildEdgesFromFieldDefs(
g graph.Graph[string, Vertex],
name string,
fields []*ast.FieldDefinition,
) {
for _, f := range fields {
// Is the field a primitive scalar (e.g., Int, String)?
// If so, we can skip it, as it's natively a part of any
// GraphQL schema.
rootType := getRootTypeNameHelper(f.Type, 0)
if isBasicType(rootType) {
continue
}

log.Debug("Found field in object",
"object", name,
"name", f.Name.Value,
"type", rootType,
)

_ = g.AddEdge(name, rootType)

// Iterate through f.Argument --
// since Fields are also dependencies themselves!
args := f.Arguments
for _, arg := range args {
root := getRootTypeNameHelper(arg.Type, 0)
if isBasicType(root) {
continue
}
_ = g.AddEdge(name, root)
}
}
}

func buildEdgesForObject(v Vertex, g graph.Graph[string, Vertex]) {
obj := v.Node.(*ast.ObjectDefinition)
fields := obj.Fields
buildEdgesFromFieldDefs(g, obj.Name.Value, fields)
}

func buildEdgesForInterface(v Vertex, g graph.Graph[string, Vertex]) {
obj := v.Node.(*ast.InterfaceDefinition)
fields := obj.Fields
buildEdgesFromFieldDefs(g, obj.Name.Value, fields)
}

func buildEdgesForUnion(v Vertex, g graph.Graph[string, Vertex]) {
// TODO add support
}

func buildEdgesForInputObject(v Vertex, g graph.Graph[string, Vertex]) {
obj := v.Node.(*ast.InputObjectDefinition)
fields := obj.Fields
name := obj.Name.Value

for _, f := range fields {
// Is the field a primitive scalar (e.g., Int, String)?
// If so, we can skip it, as it's natively a part of any
// GraphQL schema.
rootType := getRootTypeNameHelper(f.Type, 0)
if isBasicType(rootType) {
continue
}

log.Debug("Found field in object",
"object", name,
"name", f.Name.Value,
"type", rootType,
)

_ = g.AddEdge(name, rootType)
}
}
14 changes: 12 additions & 2 deletions internal/cli/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

func printSDL(doc *ast.Document) {
sdl := printer.Print(doc)
log.Infof("Printing new SDL to file with %d definitions", len(doc.Definitions))

// Create a new file where we'll write the new SDL
// TODO make configurable
Expand All @@ -22,8 +22,18 @@ func printSDL(doc *ast.Document) {

w := bufio.NewWriter(f)

_, err = w.WriteString(sdl.(string))
sdl := printer.Print(doc)

sdlString, ok := sdl.(string)
if !ok {
log.Fatal("expected SDL to be a string")
}

log.Debug(sdlString)

_, err = w.WriteString(sdlString)
if err != nil {
log.Fatal("unable to produce new SDL file", "err", err)
}
defer w.Flush()
}
6 changes: 6 additions & 0 deletions test/schema.graphql
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
"""
Representation of a "Foo"
"""
type Foo {
name: String
bar: Bar
}

"""
Representation of a "Bar
"""
type Bar {
name: String
quantity: Int!
Expand Down