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
4 changes: 2 additions & 2 deletions core/integration/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ export class Test {
require.JSONEq(t, `{"test":{"set":{"foo": "abc"}}}`, out)

_, err = modGen.With(daggerQuery(`{test{set(foo: "abc", bar: "xyz"){bar}}}`)).Stdout(ctx)
requireErrOut(t, err, `Test has no such field: "bar"`)
requireErrOut(t, err, `Cannot query field \"bar\" on type \"Test\"`)
})
}
}
Expand Down Expand Up @@ -951,7 +951,7 @@ func (ModuleSuite) TestUseLocal(ctx context.Context, t *testctx.T) {
// cannot use transitive dependency directly
_, err = modGen.With(daggerQuery(`{dep{hello}}`)).Stdout(ctx)
require.Error(t, err)
requireErrOut(t, err, `Query has no such field: "dep"`)
requireErrOut(t, err, `Cannot query field \"dep\" on type \"Query\"`)
})
}
}
Expand Down
49 changes: 24 additions & 25 deletions dagql/dagql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"time"

"github.com/99designs/gqlgen/client"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/moby/buildkit/identity"
"github.com/opencontainers/go-digest"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -67,7 +66,7 @@ func TestBasic(t *testing.T) {

points.Install[Query](srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

var res struct {
Point struct {
Expand Down Expand Up @@ -190,7 +189,7 @@ func TestNullableResults(t *testing.T) {
}),
}.Install(srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

t.Run("nullable scalars", func(t *testing.T) {
var res struct {
Expand Down Expand Up @@ -398,7 +397,7 @@ func TestListResults(t *testing.T) {
NullableListOfObjects []points.Point
}

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))
req(t, gql, `query {
listOfInts
emptyListOfInts
Expand Down Expand Up @@ -440,7 +439,7 @@ func TestLoadingFromID(t *testing.T) {

points.Install[Query](srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

var res struct {
Point struct {
Expand Down Expand Up @@ -546,7 +545,7 @@ func TestIDsReflectQuery(t *testing.T) {
srv := dagql.NewServer(Query{})
points.Install[Query](srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

var res struct {
Point struct {
Expand Down Expand Up @@ -628,7 +627,7 @@ func TestIDsDoNotContainSensitiveValues(t *testing.T) {
srv := dagql.NewServer(Query{})
points.Install[Query](srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

dagql.Fields[*points.Point]{
dagql.Func("loginTag", func(ctx context.Context, self *points.Point, _ struct {
Expand Down Expand Up @@ -737,7 +736,7 @@ func TestEmptyID(t *testing.T) {
srv := dagql.NewServer(Query{})
points.Install[Query](srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

var res struct {
LoadPointFromID struct {
Expand All @@ -759,7 +758,7 @@ func TestPureIDsDoNotReEvaluate(t *testing.T) {
srv := dagql.NewServer(Query{})
points.Install[Query](srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

called := 0
dagql.Fields[*points.Point]{
Expand Down Expand Up @@ -812,7 +811,7 @@ func TestImpureIDsReEvaluate(t *testing.T) {
srv := dagql.NewServer(Query{})
points.Install[Query](srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

called := 0
dagql.Fields[*points.Point]{
Expand Down Expand Up @@ -963,7 +962,7 @@ func TestPassingObjectsAround(t *testing.T) {
srv := dagql.NewServer(Query{})
points.Install[Query](srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

var res struct {
Point struct {
Expand Down Expand Up @@ -1000,7 +999,7 @@ func TestEnums(t *testing.T) {
srv := dagql.NewServer(Query{})
points.Install[Query](srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

t.Run("outputs", func(t *testing.T) {
var res struct {
Expand Down Expand Up @@ -1161,7 +1160,7 @@ func (BuiltinsInput) TypeName() string {

func TestInputObjects(t *testing.T) {
srv := dagql.NewServer(Query{})
gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

dagql.MustInputSpec(DefaultsInput{}).Install(srv)

Expand Down Expand Up @@ -1370,7 +1369,7 @@ func InstallDefaults(srv *dagql.Server) {

func TestDefaults(t *testing.T) {
srv := dagql.NewServer(Query{})
gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

InstallDefaults(srv)

Expand Down Expand Up @@ -1454,7 +1453,7 @@ func TestDefaults(t *testing.T) {

func TestParallelism(t *testing.T) {
srv := dagql.NewServer(Query{})
gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

pipes.Install[Query](srv)

Expand Down Expand Up @@ -1536,7 +1535,7 @@ func InstallBuiltins(srv *dagql.Server) {

func TestBuiltins(t *testing.T) {
srv := dagql.NewServer(Query{})
gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

InstallBuiltins(srv)

Expand Down Expand Up @@ -1738,7 +1737,7 @@ func TestIntrospection(t *testing.T) {
}).Meta(),
}.Install(srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

var res introspection.Response
req(t, gql, introspection.Query, &res)
Expand Down Expand Up @@ -1910,7 +1909,7 @@ func InstallViewer(srv *dagql.Server) {

func TestViews(t *testing.T) {
srv := dagql.NewServer(Query{})
gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

InstallViewer(srv)

Expand All @@ -1927,7 +1926,7 @@ func TestViews(t *testing.T) {

reqFail(t, gql, `query {
shared
}`, "no such field")
}`, "Cannot query field")
})

t.Run("in unknown view", func(t *testing.T) {
Expand All @@ -1943,7 +1942,7 @@ func TestViews(t *testing.T) {

reqFail(t, gql, `query {
shared
}`, "no such field")
}`, "Cannot query field")
})

t.Run("in first view", func(t *testing.T) {
Expand All @@ -1965,7 +1964,7 @@ func TestViews(t *testing.T) {

reqFail(t, gql, `query {
secondExclusive
}`, "no such field")
}`, "Cannot query field")
})

t.Run("in second view", func(t *testing.T) {
Expand All @@ -1987,13 +1986,13 @@ func TestViews(t *testing.T) {

reqFail(t, gql, `query {
firstExclusive
}`, "no such field")
}`, "Cannot query field")
})
}

func TestViewsCaching(t *testing.T) {
srv := dagql.NewServer(Query{})
gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

InstallViewer(srv)

Expand Down Expand Up @@ -2022,7 +2021,7 @@ func TestViewsCaching(t *testing.T) {
func TestViewsIntrospection(t *testing.T) {
srv := dagql.NewServer(Query{})
introspection.Install[Query](srv)
gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

InstallViewer(srv)

Expand Down Expand Up @@ -2153,7 +2152,7 @@ func TestCustomDigest(t *testing.T) {
}),
}.Install(srv)

gql := client.New(handler.NewDefaultServer(srv))
gql := client.New(dagql.NewDefaultHandler(srv))

// sanity test version without custom digest first
{
Expand Down
45 changes: 42 additions & 3 deletions dagql/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,35 @@ import (
"sync"

"github.com/99designs/gqlgen/graphql"
"github.com/99designs/gqlgen/graphql/errcode"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/iancoleman/strcase"
"github.com/opencontainers/go-digest"
"github.com/sourcegraph/conc/pool"
"github.com/vektah/gqlparser/v2/ast"
"github.com/vektah/gqlparser/v2/gqlerror"
"github.com/vektah/gqlparser/v2/parser"
"github.com/vektah/gqlparser/v2/validator"
"github.com/vektah/gqlparser/v2/validator/rules"
"golang.org/x/sync/errgroup"

"github.com/dagger/dagger/dagql/call"
)

func init() {
// HACK: these rules are disabled because some clients don't send the right
// types:
// - PHP + Elixir SDKs send enums quoted
// - The shell sends enums quoted, and ints/floats as strings
// - etc
validator.RemoveRule(rules.ValuesOfCorrectTypeRule.Name)
validator.RemoveRule(rules.ValuesOfCorrectTypeRuleWithoutSuggestions.Name)

// HACK: this rule is disabled because PHP modules <=v0.15.2 query
// inputArgs incorrectly.
validator.RemoveRule(rules.ScalarLeafsRule.Name)
}
Comment on lines +28 to +40
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can these be cleaned up someday? 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes! The first group can be as soon as the next release is out - they're actually fixed in this PR. But because of how we don't bundle those SDKs into the engine, we can't actually test those changes in this PR (so it would break). I think we need to fix this somehow, but kinda still thinking through ideas.

The second group is pending an @dagger/sdk-php fix: https://discord.com/channels/707636530424053791/1162025276872609832/1333814026731262012


// Server represents a GraphQL server whose schema is dynamically modified at
// runtime.
type Server struct {
Expand Down Expand Up @@ -98,6 +116,11 @@ func NewServer[T Typed](root T) *Server {
return srv
}

func NewDefaultHandler(es graphql.ExecutableSchema) *handler.Server {
// TODO: avoid this deprecated method, and customize the options
return handler.NewDefaultServer(es) //nolint: staticcheck
}

var coreScalars = []ScalarType{
Boolean(false),
Int(0),
Expand Down Expand Up @@ -317,19 +340,27 @@ func (s *Server) Schema() *ast.Schema { // TODO: change this to be updated whene
defer s.installLock.Unlock()
// TODO track when the schema changes, cache until it changes again
queryType := s.Root().Type().Name()
schema := &ast.Schema{}
schema := &ast.Schema{
Types: make(map[string]*ast.Definition),
PossibleTypes: make(map[string][]*ast.Definition),
}
for _, t := range s.objects { // TODO stable order
def := definition(ast.Object, t, s.View)
if def.Name == queryType {
schema.Query = def
}
schema.AddTypes(def)
schema.AddPossibleType(def.Name, def)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this right? I think "possible types" is meant to be a mapping from unions and interfaces to their concrete implementations, whereas this seems to imply every type has itself as a possible type

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I don't know actually 🤔

But this validation rule implies that it is meant to be set like this? https://github.com/vektah/gqlparser/blob/e21b122b4e0ebb42ad98fe4ecf738bf380e487a3/validator/rules/possible_fragment_spreads.go#L36-L36

Copy link
Copy Markdown
Contributor

@vito vito Jan 27, 2025

Choose a reason for hiding this comment

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

Does it fail without it?

The linked is regarding fragment spreads which should only be relevant to type switching on interfaces/unions, but it's for sure weird that it seems to pass the spreaded (concrete) type to GetPossibleTypes considering its own godoc says it's only for interfaces/unions:

// GetPossibleTypes will enumerate all the definitions for a given interface or union
func (s *Schema) GetPossibleTypes(def *Definition) []*Definition {
	return s.PossibleTypes[def.Name]
}

If it doesn't fail I would just skip it - we don't use interfaces anyway (and I don't think we use unions either). My main concern is that it might make the schema introspection a bit funky

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It does fail, which was the reason I actually caught that linting wasn't ever enabled in the first place:

TransportQueryError: Error while fetching schema: {'message': 'Fragment 
"TypeRef" cannot be spread here as objects of type "__Type" can never be of type
"__Type".', 'locations': [{'line': 63, 'column': 8}], 'extensions': {'code': 
'GRAPHQL_VALIDATION_FAILED'}}

From https://github.com/vektah/gqlparser/blob/e21b122b4e0ebb42ad98fe4ecf738bf380e487a3/validator/rules/possible_fragment_spreads.go#L58-L68

I think potentially maybe this is just... wrong? Might submit a patch upstream.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Ah, this is why it works - as part of loading the schema, it adds all Types to PossibleTypes 🤔 https://github.com/vektah/gqlparser/blob/28ef20ae1564c8a8ac8a0305711e0122fe937e9a/validator/schema.go#L74-L74

So we should do the same thing here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Alright - seems weird, but I'm convinced it's at least consistent 😛

}
for _, t := range s.scalars {
schema.AddTypes(definition(ast.Scalar, t, s.View))
def := definition(ast.Scalar, t, s.View)
schema.AddTypes(def)
schema.AddPossibleType(def.Name, def)
}
for _, t := range s.typeDefs {
schema.AddTypes(t.TypeDefinition(s.View))
def := t.TypeDefinition(s.View)
schema.AddTypes(def)
schema.AddPossibleType(def.Name, def)
}
schema.Directives = map[string]*ast.DirectiveDefinition{}
for n, d := range s.directives {
Expand Down Expand Up @@ -398,6 +429,14 @@ func (s *Server) ExecOp(ctx context.Context, gqlOp *graphql.OperationContext) (m
if err != nil {
return nil, gqlErrs(err)
}

listErr := validator.Validate(s.Schema(), gqlOp.Doc)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yay!

if len(listErr) != 0 {
for _, e := range listErr {
errcode.Set(e, errcode.ValidationFailed)
}
return nil, listErr
}
}
results := make(map[string]any)
for _, op := range gqlOp.Doc.Operations {
Expand Down
3 changes: 1 addition & 2 deletions engine/server/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"time"

"dagger.io/dagger/telemetry"
"github.com/99designs/gqlgen/graphql/handler"
"github.com/Khan/genqlient/graphql"
"github.com/containerd/containerd/content"
"github.com/koron-go/prefixw"
Expand Down Expand Up @@ -1090,7 +1089,7 @@ func (srv *Server) serveQuery(w http.ResponseWriter, r *http.Request, client *da
return gqlErr(fmt.Errorf("failed to get schema: %w", err), http.StatusBadRequest)
}

gqlSrv := handler.NewDefaultServer(schema)
gqlSrv := dagql.NewDefaultHandler(schema)
// NB: break glass when needed:
// gqlSrv.AroundResponses(func(ctx context.Context, next graphql.ResponseHandler) *graphql.Response {
// res := next(ctx)
Expand Down
Loading