From aacc9b1fd6ff8fa91d4b4985b4c115796851b416 Mon Sep 17 00:00:00 2001 From: Adam Date: Sat, 8 Feb 2020 17:18:02 +1100 Subject: [PATCH] Remove source reprinting --- api/generate.go | 21 +- codegen/config/config.go | 39 +- codegen/data.go | 12 +- codegen/generated!.gotpl | 11 +- codegen/testserver/generated.go | 723 +++++++++++------- docs/content/recipes/federation.md | 9 +- example/chat/generated.go | 39 +- example/config/generated.go | 47 +- example/dataloader/generated.go | 49 +- .../accounts/graph/generated/federation.go | 20 +- .../accounts/graph/generated/generated.go | 61 +- .../products/graph/generated/federation.go | 21 +- .../products/graph/generated/generated.go | 65 +- .../reviews/graph/generated/federation.go | 26 +- .../reviews/graph/generated/generated.go | 77 +- .../federation/reviews/graph/schema.graphqls | 4 +- example/fileupload/generated.go | 60 +- example/scalars/generated.go | 60 +- example/selection/generated.go | 37 +- example/starwars/generated/exec.go | 189 +++-- example/todo/generated.go | 72 +- example/type-system-extension/generated.go | 102 ++- go.mod | 2 +- go.sum | 4 +- integration/generated.go | 79 +- plugin/federation/federation.go | 206 ++--- plugin/federation/federation.gotpl | 13 +- plugin/federation/federation_test.go | 82 +- plugin/federation/test_data/schema.graphql | 5 + plugin/plugin.go | 10 +- 30 files changed, 1163 insertions(+), 982 deletions(-) diff --git a/api/generate.go b/api/generate.go index da148540743..fd81cb08813 100644 --- a/api/generate.go +++ b/api/generate.go @@ -32,25 +32,30 @@ func Generate(cfg *config.Config, option ...Option) error { } for _, p := range plugins { - if inj, ok := p.(plugin.SourcesInjector); ok { - inj.InjectSources(cfg) + if inj, ok := p.(plugin.EarlySourceInjector); ok { + if s := inj.InjectSourceEarly(); s != nil { + cfg.Sources = append(cfg.Sources, s) + } } } - err := cfg.LoadSchema() - if err != nil { + if err := cfg.LoadSchema(); err != nil { return errors.Wrap(err, "failed to load schema") } for _, p := range plugins { - if mut, ok := p.(plugin.SchemaMutator); ok { - err := mut.MutateSchema(cfg.Schema) - if err != nil { - return errors.Wrap(err, p.Name()) + if inj, ok := p.(plugin.LateSourceInjector); ok { + if s := inj.InjectSourceLate(cfg.Schema); s != nil { + cfg.Sources = append(cfg.Sources, s) } } } + // LoadSchema again now we have everything + if err := cfg.LoadSchema(); err != nil { + return errors.Wrap(err, "failed to load schema") + } + if err := cfg.Init(); err != nil { return errors.Wrap(err, "generating core failed") } diff --git a/codegen/config/config.go b/codegen/config/config.go index f0a7f14f7c2..c2581caab57 100644 --- a/codegen/config/config.go +++ b/codegen/config/config.go @@ -28,7 +28,7 @@ type Config struct { Directives map[string]DirectiveConfig `yaml:"directives,omitempty"` OmitSliceElementPointers bool `yaml:"omit_slice_element_pointers,omitempty"` SkipValidation bool `yaml:"skip_validation,omitempty"` - AdditionalSources []*ast.Source `yaml:"-"` + Sources []*ast.Source `yaml:"-"` Packages *code.Packages `yaml:"-"` Schema *ast.Schema `yaml:"-"` @@ -138,6 +138,19 @@ func LoadConfig(filename string) (*Config, error) { } } + for _, filename := range config.SchemaFilename { + filename = filepath.ToSlash(filename) + var err error + var schemaRaw []byte + schemaRaw, err = ioutil.ReadFile(filename) + if err != nil { + fmt.Fprintln(os.Stderr, "unable to open schema: "+err.Error()) + os.Exit(1) + } + + config.Sources = append(config.Sources, &ast.Source{Name: filename, Input: string(schemaRaw)}) + } + return config, nil } @@ -569,23 +582,19 @@ func (c *Config) LoadSchema() error { return err } - sources := append([]*ast.Source{}, c.AdditionalSources...) - for _, filename := range c.SchemaFilename { - filename = filepath.ToSlash(filename) - var err error - var schemaRaw []byte - schemaRaw, err = ioutil.ReadFile(filename) - if err != nil { - fmt.Fprintln(os.Stderr, "unable to open schema: "+err.Error()) - os.Exit(1) - } - sources = append(sources, &ast.Source{Name: filename, Input: string(schemaRaw)}) - } - - schema, err := gqlparser.LoadSchema(sources...) + schema, err := gqlparser.LoadSchema(c.Sources...) if err != nil { return err } + + if schema.Query == nil { + schema.Query = &ast.Definition{ + Kind: ast.Object, + Name: "Query", + } + schema.Types["Query"] = schema.Query + } + c.Schema = schema return nil } diff --git a/codegen/data.go b/codegen/data.go index e30b33c83c1..2f0ec3fb4e0 100644 --- a/codegen/data.go +++ b/codegen/data.go @@ -1,14 +1,13 @@ package codegen import ( - "bytes" "fmt" "sort" - "github.com/99designs/gqlgen/codegen/config" "github.com/pkg/errors" "github.com/vektah/gqlparser/ast" - "github.com/vektah/gqlparser/formatter" + + "github.com/99designs/gqlgen/codegen/config" ) // Data is a unified model of the code to be generated. Plugins may modify this structure to do things like implement @@ -16,7 +15,6 @@ import ( type Data struct { Config *config.Config Schema *ast.Schema - SchemaStr map[string]string Directives DirectiveList Objects Objects Inputs Objects @@ -32,7 +30,6 @@ type Data struct { type builder struct { Config *config.Config Schema *ast.Schema - SchemaStr map[string]string Binder *config.Binder Directives map[string]*Directive } @@ -62,7 +59,6 @@ func BuildData(cfg *config.Config) (*Data, error) { Config: cfg, Directives: dataDirectives, Schema: b.Schema, - SchemaStr: b.SchemaStr, Interfaces: map[string]*Interface{}, } @@ -127,10 +123,6 @@ func BuildData(cfg *config.Config) (*Data, error) { return nil, fmt.Errorf("invalid types were encountered while traversing the go source code, this probably means the invalid code generated isnt correct. add try adding -v to debug") } - var buf bytes.Buffer - formatter.NewFormatter(&buf).FormatSchema(b.Schema) - s.SchemaStr = map[string]string{"schema.graphql": buf.String()} - return &s, nil } diff --git a/codegen/generated!.gotpl b/codegen/generated!.gotpl index a51f295d54f..bcfab0fa943 100644 --- a/codegen/generated!.gotpl +++ b/codegen/generated!.gotpl @@ -206,8 +206,9 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - {{- range $filename, $schema := .SchemaStr }} - &ast.Source{Name: {{$filename|quote}}, Input: {{$schema|rawQuote}}}, - {{- end }} -) +var sources = []*ast.Source{ +{{- range $source := .Config.Sources }} + &ast.Source{Name: {{$source.Name|quote}}, Input: {{$source.Input|rawQuote}}, BuiltIn: {{$source.BuiltIn}}}, +{{- end }} +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) diff --git a/codegen/testserver/generated.go b/codegen/testserver/generated.go index 04d448f1d86..bdddfe6bd44 100644 --- a/codegen/testserver/generated.go +++ b/codegen/testserver/generated.go @@ -1663,366 +1663,511 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `directive @custom on ARGUMENT_DEFINITION -directive @directive1 on FIELD_DEFINITION -directive @directive2 on FIELD_DEFINITION -directive @goField(forceResolver: Boolean, name: String) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION -directive @goModel(model: String, models: [String!]) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION -directive @length(min: Int!, max: Int, message: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION -directive @logged(id: UUID!) on FIELD -directive @makeNil on FIELD_DEFINITION -directive @makeTypedNil on FIELD_DEFINITION +var sources = []*ast.Source{ + &ast.Source{Name: "builtinscalar.graphql", Input: ` +""" +Since gqlgen defines default implementation for a Map scalar, this tests that the builtin is _not_ +added to the TypeMap +""" +type Map { + id: ID! +} +`, BuiltIn: false}, + &ast.Source{Name: "complexity.graphql", Input: `extend type Query { + overlapping: OverlappingFields +} + +type OverlappingFields { + oneFoo: Int! @goField(name: "foo") + twoFoo: Int! @goField(name: "foo") + oldFoo: Int! @goField(name: "foo", forceResolver: true) + newFoo: Int! + new_foo: Int! +} +`, BuiltIn: false}, + &ast.Source{Name: "directive.graphql", Input: `directive @length(min: Int!, max: Int, message: String) on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION directive @range(min: Int = 0, max: Int) on ARGUMENT_DEFINITION +directive @custom on ARGUMENT_DEFINITION +directive @logged(id: UUID!) on FIELD directive @toNull on ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | FIELD_DEFINITION +directive @directive1 on FIELD_DEFINITION +directive @directive2 on FIELD_DEFINITION directive @unimplemented on FIELD_DEFINITION -type A { - id: ID! + +extend type Query { + directiveArg(arg: String! @length(min:1, max: 255, message: "invalid length")): String + directiveNullableArg(arg: Int @range(min:0), arg2: Int @range, arg3: String @toNull): String + directiveInputNullable(arg: InputDirectives): String + directiveInput(arg: InputDirectives!): String + directiveInputType(arg: InnerInput! @custom): String + directiveObject: ObjectDirectives + directiveObjectWithCustomGoModel: ObjectDirectivesWithCustomGoModel + directiveFieldDef(ret: String!): String! @length(min: 1, message: "not valid") + directiveField: String + directiveDouble: String @directive1 @directive2 + directiveUnimplemented: String @unimplemented } -type AIt { - id: ID! + +extend type Subscription { + directiveArg(arg: String! @length(min:1, max: 255, message: "invalid length")): String + directiveNullableArg(arg: Int @range(min:0), arg2: Int @range, arg3: String @toNull): String + directiveDouble: String @directive1 @directive2 + directiveUnimplemented: String @unimplemented } -type AbIt { - id: ID! + +input InputDirectives { + text: String! @length(min: 0, max: 7, message: "not valid") + nullableText: String @toNull + inner: InnerDirectives! + innerNullable: InnerDirectives + thirdParty: ThirdParty @length(min: 0, max: 7) } -interface Animal { - species: String! + +input InnerDirectives { + message: String! @length(min: 1, message: "not valid") } -type Autobind { - int: Int! - int32: Int! - int64: Int! - idStr: ID! - idInt: ID! + +type ObjectDirectives { + text: String! @length(min: 0, max: 7, message: "not valid") + nullableText: String @toNull } -type B { - id: ID! + +type ObjectDirectivesWithCustomGoModel { + nullableText: String @toNull +} +`, BuiltIn: false}, + &ast.Source{Name: "embedded.graphql", Input: `extend type Query { + embeddedCase1: EmbeddedCase1 + embeddedCase2: EmbeddedCase2 + embeddedCase3: EmbeddedCase3 +} + +type EmbeddedCase1 @goModel(model:"testserver.EmbeddedCase1") { + exportedEmbeddedPointerExportedMethod: String! +} + +type EmbeddedCase2 @goModel(model:"testserver.EmbeddedCase2") { + unexportedEmbeddedPointerExportedMethod: String! +} + +type EmbeddedCase3 @goModel(model:"testserver.EmbeddedCase3") { + unexportedEmbeddedInterfaceExportedMethod: String! +} +`, BuiltIn: false}, + &ast.Source{Name: "enum.graphql", Input: `enum EnumTest { + OK + NG } + +input InputWithEnumValue { + enum: EnumTest! +} + +extend type Query { + enumInInput(input: InputWithEnumValue): EnumTest! +} +`, BuiltIn: false}, + &ast.Source{Name: "interfaces.graphql", Input: `extend type Query { + shapes: [Shape] + noShape: Shape @makeNil + node: Node! + noShapeTypedNil: Shape @makeTypedNil + animal: Animal @makeTypedNil + notAnInterface: BackedByInterface +} + +interface Animal { + species: String! +} + type BackedByInterface { - id: String! - thisShouldBind: String! - thisShouldBindWithError: String! + id: String! + thisShouldBind: String! + thisShouldBindWithError: String! } -scalar Bytes -type Cat implements Animal { - species: String! - catBreed: String! + +type Dog implements Animal { + species: String! + dogBreed: String! } -input Changes @goModel(model: "map[string]interface{}") { - a: Int - b: Int + +type Cat implements Animal { + species: String! + catBreed: String! } -type CheckIssue896 { - id: Int + +interface Shape { + area: Float } type Circle implements Shape { - radius: Float - area: Float + radius: Float + area: Float } -type ConcreteNodeA implements Node { - id: ID! - child: Node! - name: String! +type Rectangle implements Shape { + length: Float + width: Float + area: Float } -union Content_Child = Content_User | Content_Post -type Content_Post { - foo: String +union ShapeUnion @goModel(model:"testserver.ShapeUnion") = Circle | Rectangle + +directive @makeNil on FIELD_DEFINITION +directive @makeTypedNil on FIELD_DEFINITION + +interface Node { + id: ID! + child: Node! } -type Content_User { - foo: String + +type ConcreteNodeA implements Node { + id: ID! + child: Node! + name: String! } -""" - This doesnt have an implementation in the typemap, so it should act like a string -""" -scalar DefaultScalarImplementation -type Dog implements Animal { - species: String! - dogBreed: String! +`, BuiltIn: false}, + &ast.Source{Name: "issue896.graphql", Input: `# This example should build stable output. If the file content starts +# alternating nondeterministically between two outputs, then see +# https://github.com/99designs/gqlgen/issues/896. + +extend schema { + query: Query + subscription: Subscription } -type EmbeddedCase1 @goModel(model: "testserver.EmbeddedCase1") { - exportedEmbeddedPointerExportedMethod: String! + +type CheckIssue896 {id: Int} + +extend type Query { + issue896a: [CheckIssue896!] # Note the "!" or lack thereof. } -type EmbeddedCase2 @goModel(model: "testserver.EmbeddedCase2") { - unexportedEmbeddedPointerExportedMethod: String! + +extend type Subscription { + issue896b: [CheckIssue896] # Note the "!" or lack thereof. } -type EmbeddedCase3 @goModel(model: "testserver.EmbeddedCase3") { - unexportedEmbeddedInterfaceExportedMethod: String! +`, BuiltIn: false}, + &ast.Source{Name: "loops.graphql", Input: `type LoopA { + b: LoopB! } -type EmbeddedDefaultScalar { - value: DefaultScalarImplementation + +type LoopB { + a: LoopA! } -type EmbeddedPointer @goModel(model: "testserver.EmbeddedPointerModel") { - ID: String - Title: String +`, BuiltIn: false}, + &ast.Source{Name: "maps.graphql", Input: `extend type Query { + mapStringInterface(in: MapStringInterfaceInput): MapStringInterfaceType + mapNestedStringInterface(in: NestedMapInput): MapStringInterfaceType } -enum EnumTest { - OK - NG + +type MapStringInterfaceType @goModel(model: "map[string]interface{}") { + a: String + b: Int } -type Error { - id: ID! - errorOnNonRequiredField: String - errorOnRequiredField: String! - nilOnRequiredField: String! + +input MapStringInterfaceInput @goModel(model: "map[string]interface{}") { + a: String + b: Int } -type Errors { - a: Error! - b: Error! - c: Error! - d: Error! - e: Error! + +input NestedMapInput { + map: MapStringInterfaceInput } -enum FallbackToStringEncoding { - A - B - C +`, BuiltIn: false}, + &ast.Source{Name: "nulls.graphql", Input: `extend type Query { + errorBubble: Error + errors: Errors + valid: String! } -type ForcedResolver { - field: Circle @goField(forceResolver: true) + +type Errors { + a: Error! + b: Error! + c: Error! + d: Error! + e: Error! } -input InnerDirectives { - message: String! @length(min: 1, message: "not valid") + +type Error { + id: ID! + errorOnNonRequiredField: String + errorOnRequiredField: String! + nilOnRequiredField: String! } -input InnerInput { - id: Int! +`, BuiltIn: false}, + &ast.Source{Name: "panics.graphql", Input: `extend type Query { + panics: Panics } -type InnerObject { - id: Int! + +type Panics { + fieldScalarMarshal: [MarshalPanic!]! + fieldFuncMarshal(u: [MarshalPanic!]!): [MarshalPanic!]! + argUnmarshal(u: [MarshalPanic!]!): Boolean! + } -input InputDirectives { - text: String! @length(min: 0, max: 7, message: "not valid") - nullableText: String @toNull - inner: InnerDirectives! - innerNullable: InnerDirectives - thirdParty: ThirdParty @length(min: 0, max: 7) + +scalar MarshalPanic +`, BuiltIn: false}, + &ast.Source{Name: "primitive_objects.graphql", Input: `extend type Query { + primitiveObject: [Primitive!]! + primitiveStringObject: [PrimitiveString!]! } -input InputWithEnumValue { - enum: EnumTest! + +type Primitive { + value: Int! + squared: Int! } -type InvalidIdentifier { - id: Int! + +type PrimitiveString { + value: String! + doubled: String! + len: Int! } -type It { - id: ID! +`, BuiltIn: false}, + &ast.Source{Name: "scalar_default.graphql", Input: `extend type Query { + defaultScalar(arg: DefaultScalarImplementation! = "default"): DefaultScalarImplementation! } -type LoopA { - b: LoopB! + +""" This doesnt have an implementation in the typemap, so it should act like a string """ +scalar DefaultScalarImplementation + +type EmbeddedDefaultScalar { + value: DefaultScalarImplementation } -type LoopB { - a: LoopA! +`, BuiltIn: false}, + &ast.Source{Name: "schema.graphql", Input: `directive @goModel(model: String, models: [String!]) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION +directive @goField(forceResolver: Boolean, name: String) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION + +type Query { + invalidIdentifier: InvalidIdentifier + collision: It + mapInput(input: Changes): Boolean + recursive(input: RecursiveInputSlice): Boolean + nestedInputs(input: [[OuterInput]] = [[{inner: {id: 1}}]]): Boolean + nestedOutputs: [[OuterObject]] + modelMethods: ModelMethods + user(id: Int!): User! + nullableArg(arg: Int = 123): String + inputSlice(arg: [String!]!): Boolean! + shapeUnion: ShapeUnion! + autobind: Autobind + deprecatedField: String! @deprecated(reason: "test deprecated directive") } -""" -Since gqlgen defines default implementation for a Map scalar, this tests that the builtin is _not_ -added to the TypeMap -""" -type Map { - id: ID! + +type Subscription { + updated: String! + initPayload: String! } -input MapStringInterfaceInput @goModel(model: "map[string]interface{}") { - a: String - b: Int + +type User { + id: Int! + friends: [User!]! @goField(forceResolver: true) + created: Time! + updated: Time } -type MapStringInterfaceType @goModel(model: "map[string]interface{}") { - a: String - b: Int + +type Autobind { + int: Int! + int32: Int! + int64: Int! + + idStr: ID! + idInt: ID! } -scalar MarshalPanic + type ModelMethods { - resolverField: Boolean! - noContext: Boolean! - withContext: Boolean! + resolverField: Boolean! + noContext: Boolean! + withContext: Boolean! } -input NestedMapInput { - map: MapStringInterfaceInput + +type InvalidIdentifier { + id: Int! } -interface Node { - id: ID! - child: Node! + +type It { + id: ID! } -type ObjectDirectives { - text: String! @length(min: 0, max: 7, message: "not valid") - nullableText: String @toNull + +input Changes @goModel(model:"map[string]interface{}") { + a: Int + b: Int } -type ObjectDirectivesWithCustomGoModel { - nullableText: String @toNull + +input RecursiveInputSlice { + self: [RecursiveInputSlice!] +} + +input InnerInput { + id:Int! } + input OuterInput { - inner: InnerInput! + inner: InnerInput! } + +scalar ThirdParty @goModel(model:"testserver.ThirdParty") + type OuterObject { - inner: InnerObject! + inner: InnerObject! } -type OverlappingFields { - oneFoo: Int! @goField(name: "foo") - twoFoo: Int! @goField(name: "foo") - oldFoo: Int! @goField(name: "foo", forceResolver: true) - newFoo: Int! - new_foo: Int! -} -type Panics { - fieldScalarMarshal: [MarshalPanic!]! - fieldFuncMarshal(u: [MarshalPanic!]!): [MarshalPanic!]! - argUnmarshal(u: [MarshalPanic!]!): Boolean! + +type InnerObject { + id: Int! } -type Primitive { - value: Int! - squared: Int! + +type ForcedResolver { + field: Circle @goField(forceResolver: true) } -type PrimitiveString { - value: String! - doubled: String! - len: Int! + +type EmbeddedPointer @goModel(model:"testserver.EmbeddedPointerModel") { + ID: String + Title: String } -type Query { - invalidIdentifier: InvalidIdentifier - collision: It - mapInput(input: Changes): Boolean - recursive(input: RecursiveInputSlice): Boolean - nestedInputs(input: [[OuterInput]] = [[{inner:{id:1}}]]): Boolean - nestedOutputs: [[OuterObject]] - modelMethods: ModelMethods - user(id: Int!): User! - nullableArg(arg: Int = 123): String - inputSlice(arg: [String!]!): Boolean! - shapeUnion: ShapeUnion! - autobind: Autobind - deprecatedField: String! @deprecated(reason: "test deprecated directive") - overlapping: OverlappingFields - directiveArg(arg: String!): String - directiveNullableArg(arg: Int, arg2: Int, arg3: String): String - directiveInputNullable(arg: InputDirectives): String - directiveInput(arg: InputDirectives!): String - directiveInputType(arg: InnerInput!): String - directiveObject: ObjectDirectives - directiveObjectWithCustomGoModel: ObjectDirectivesWithCustomGoModel - directiveFieldDef(ret: String!): String! @length(min: 1, message: "not valid") - directiveField: String - directiveDouble: String @directive1 @directive2 - directiveUnimplemented: String @unimplemented - embeddedCase1: EmbeddedCase1 - embeddedCase2: EmbeddedCase2 - embeddedCase3: EmbeddedCase3 - enumInInput(input: InputWithEnumValue): EnumTest! - shapes: [Shape] - noShape: Shape @makeNil - node: Node! - noShapeTypedNil: Shape @makeTypedNil - animal: Animal @makeTypedNil - notAnInterface: BackedByInterface - issue896a: [CheckIssue896!] - mapStringInterface(in: MapStringInterfaceInput): MapStringInterfaceType - mapNestedStringInterface(in: NestedMapInput): MapStringInterfaceType - errorBubble: Error - errors: Errors - valid: String! - panics: Panics - primitiveObject: [Primitive!]! - primitiveStringObject: [PrimitiveString!]! - defaultScalar(arg: DefaultScalarImplementation! = "default"): DefaultScalarImplementation! - slices: Slices - scalarSlice: Bytes! - fallback(arg: FallbackToStringEncoding!): FallbackToStringEncoding! - optionalUnion: TestUnion - validType: ValidType - wrappedStruct: WrappedStruct! - wrappedScalar: WrappedScalar! + +scalar UUID + +enum Status { + OK + ERROR } -type Rectangle implements Shape { - length: Float - width: Float - area: Float + +scalar Time +`, BuiltIn: false}, + &ast.Source{Name: "slices.graphql", Input: `extend type Query { + slices: Slices + scalarSlice: Bytes! } -input RecursiveInputSlice { - self: [RecursiveInputSlice!] + +type Slices { + test1: [String] + test2: [String!] + test3: [String]! + test4: [String!]! } -interface Shape { - area: Float + +scalar Bytes +`, BuiltIn: false}, + &ast.Source{Name: "typefallback.graphql", Input: `extend type Query { + fallback(arg: FallbackToStringEncoding!): FallbackToStringEncoding! } -union ShapeUnion @goModel(model: "testserver.ShapeUnion") = Circle | Rectangle -type Slices { - test1: [String] - test2: [String!] - test3: [String]! - test4: [String!]! + +enum FallbackToStringEncoding { + A + B + C } -enum Status { - OK - ERROR +`, BuiltIn: false}, + &ast.Source{Name: "useptr.graphql", Input: `type A { + id: ID! } -type Subscription { - updated: String! - initPayload: String! - directiveArg(arg: String!): String - directiveNullableArg(arg: Int, arg2: Int, arg3: String): String - directiveDouble: String @directive1 @directive2 - directiveUnimplemented: String @unimplemented - issue896b: [CheckIssue896] + +type B { + id: ID! } + union TestUnion = A | B -scalar ThirdParty @goModel(model: "testserver.ThirdParty") -scalar Time -scalar UUID -type User { - id: Int! - friends: [User!]! @goField(forceResolver: true) - created: Time! - updated: Time + +extend type Query { + optionalUnion: TestUnion } -input ValidInput { - break: String! - default: String! - func: String! - interface: String! - select: String! - case: String! - defer: String! - go: String! - map: String! - struct: String! - chan: String! - else: String! - goto: String! - package: String! - switch: String! - const: String! - fallthrough: String! - if: String! - range: String! - type: String! - continue: String! - for: String! - import: String! - return: String! - var: String! - _: String! @goField(name: "Underscore") +`, BuiltIn: false}, + &ast.Source{Name: "validtypes.graphql", Input: `extend type Query { + validType: ValidType } -""" - These things are all valid, but without care generate invalid go code -""" + +""" These things are all valid, but without care generate invalid go code """ type ValidType { - differentCase: String! - different_case: String! @goField(name: "DifferentCaseOld") - validInputKeywords(input: ValidInput): Boolean! - validArgs(break: String!, default: String!, func: String!, interface: String!, select: String!, case: String!, defer: String!, go: String!, map: String!, struct: String!, chan: String!, else: String!, goto: String!, package: String!, switch: String!, const: String!, fallthrough: String!, if: String!, range: String!, type: String!, continue: String!, for: String!, import: String!, return: String!, var: String!, _: String!): Boolean! -} -scalar WrappedScalar -type WrappedStruct { - name: String! + differentCase: String! + different_case: String! @goField(name:"DifferentCaseOld") + validInputKeywords(input: ValidInput): Boolean! + validArgs( + break: String!, + default: String!, + func: String!, + interface: String!, + select: String!, + case: String!, + defer: String!, + go: String!, + map: String!, + struct: String!, + chan: String!, + else: String!, + goto: String!, + package: String!, + switch: String!, + const: String!, + fallthrough: String!, + if: String!, + range: String!, + type: String!, + continue: String!, + for: String!, + import: String!, + return: String!, + var: String!, + _: String!, + ): Boolean! } -type XXIt { - id: ID! + +input ValidInput { + break: String! + default: String! + func: String! + interface: String! + select: String! + case: String! + defer: String! + go: String! + map: String! + struct: String! + chan: String! + else: String! + goto: String! + package: String! + switch: String! + const: String! + fallthrough: String! + if: String! + range: String! + type: String! + continue: String! + for: String! + import: String! + return: String! + var: String! + _: String! @goField(name: "Underscore") +} + +# see https://github.com/99designs/gqlgen/issues/694 +type Content_User { + foo: String } -type XxIt { - id: ID! + +type Content_Post { + foo: String } -type asdfIt { - id: ID! + +union Content_Child = Content_User | Content_Post +`, BuiltIn: false}, + &ast.Source{Name: "weird_type_cases.graphql", Input: `# regression test for https://github.com/99designs/gqlgen/issues/583 + +type asdfIt { id: ID! } +type iIt { id: ID! } +type AIt { id: ID! } +type XXIt { id: ID! } +type AbIt { id: ID! } +type XxIt { id: ID! } +`, BuiltIn: false}, + &ast.Source{Name: "wrapped_type.graphql", Input: `# regression test for https://github.com/99designs/gqlgen/issues/721 + +extend type Query { + wrappedStruct: WrappedStruct! + wrappedScalar: WrappedScalar! } -type iIt { - id: ID! + +type WrappedStruct { name: String! } +scalar WrappedScalar +`, BuiltIn: false}, } -`}, -) +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/docs/content/recipes/federation.md b/docs/content/recipes/federation.md index 70b52863061..91e3f23a3d2 100644 --- a/docs/content/recipes/federation.md +++ b/docs/content/recipes/federation.md @@ -25,22 +25,17 @@ type Review { product: Product } -type User @extends @key(fields: "id") { +extend type User @key(fields: "id") { id: ID! @external reviews: [Review] } -type Product @extends @key(fields: "upc") { +extend type Product @key(fields: "upc") { upc: String! @external reviews: [Review] } ``` -> Note -> -> gqlgen doesnt currently support `extend type Foo` syntax for apollo federation, we must use -> the `@extends` directive instead. - and regenerate ```bash diff --git a/example/chat/generated.go b/example/chat/generated.go index 96aea318838..17657a5ec06 100644 --- a/example/chat/generated.go +++ b/example/chat/generated.go @@ -255,30 +255,37 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `directive @user(username: String!) on SUBSCRIPTION -type Chatroom { - name: String! - messages: [Message!]! +var sources = []*ast.Source{ + &ast.Source{Name: "schema.graphql", Input: `type Chatroom { + name: String! + messages: [Message!]! } + type Message { - id: ID! - text: String! - createdBy: String! - createdAt: Time! -} -type Mutation { - post(text: String!, username: String!, roomName: String!): Message! + id: ID! + text: String! + createdBy: String! + createdAt: Time! } + type Query { - room(name: String!): Chatroom + room(name:String!): Chatroom +} + +type Mutation { + post(text: String!, username: String!, roomName: String!): Message! } + type Subscription { - messageAdded(roomName: String!): Message! + messageAdded(roomName: String!): Message! } + scalar Time -`}, -) + +directive @user(username: String!) on SUBSCRIPTION +`, BuiltIn: false}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/example/config/generated.go b/example/config/generated.go index 5300c739948..2f42596f7fb 100644 --- a/example/config/generated.go +++ b/example/config/generated.go @@ -221,32 +221,39 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `directive @goField(forceResolver: Boolean, name: String) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION -directive @goModel(model: String, models: [String!]) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION +var sources = []*ast.Source{ + &ast.Source{Name: "schema.graphql", Input: `directive @goModel(model: String, models: [String!]) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION +directive @goField(forceResolver: Boolean, name: String) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION + +type Query { + todos: [Todo!]! +} + type Mutation { - createTodo(input: NewTodo!): Todo! + createTodo(input: NewTodo!): Todo! } -input NewTodo { - text: String! - userId: String! +`, BuiltIn: false}, + &ast.Source{Name: "todo.graphql", Input: `type Todo { + id: ID! @goField(forceResolver: true) + databaseId: Int! + text: String! + done: Boolean! + user: User! } -type Query { - todos: [Todo!]! + +input NewTodo { + text: String! + userId: String! } -type Todo { - id: ID! @goField(forceResolver: true) - databaseId: Int! - text: String! - done: Boolean! - user: User! +`, BuiltIn: false}, + &ast.Source{Name: "user.graphql", Input: `type User +@goModel(model:"github.com/99designs/gqlgen/example/config.User") { + id: ID! + name: String! @goField(name:"FullName") } -type User @goModel(model: "github.com/99designs/gqlgen/example/config.User") { - id: ID! - name: String! @goField(name: "FullName") +`, BuiltIn: false}, } -`}, -) +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/example/dataloader/generated.go b/example/dataloader/generated.go index 2d7715d1cf1..9c5fd599828 100644 --- a/example/dataloader/generated.go +++ b/example/dataloader/generated.go @@ -267,35 +267,42 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `type Address { - id: Int! - street: String! - country: String! +var sources = []*ast.Source{ + &ast.Source{Name: "schema.graphql", Input: `type Query { + customers: [Customer!] + + # these methods are here to test code generation of nested arrays + torture1d(customerIds: [Int!]): [Customer!] + torture2d(customerIds: [[Int!]]): [[Customer!]] } + type Customer { - id: Int! - name: String! - address: Address - orders: [Order!] + id: Int! + name: String! + address: Address + orders: [Order!] } -type Item { - name: String! + +type Address { + id: Int! + street: String! + country: String! } + type Order { - id: Int! - date: Time! - amount: Float! - items: [Item!] + id: Int! + date: Time! + amount: Float! + items: [Item!] } -type Query { - customers: [Customer!] - torture1d(customerIds: [Int!]): [Customer!] - torture2d(customerIds: [[Int!]]): [[Customer!]] + +type Item { + name: String! } scalar Time -`}, -) +`, BuiltIn: false}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/example/federation/accounts/graph/generated/federation.go b/example/federation/accounts/graph/generated/federation.go index be978f1b543..6c574a8a863 100644 --- a/example/federation/accounts/graph/generated/federation.go +++ b/example/federation/accounts/graph/generated/federation.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "strings" "github.com/99designs/gqlgen/plugin/federation/fedruntime" ) @@ -14,15 +15,18 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. if ec.DisableIntrospection { return fedruntime.Service{}, errors.New("federated introspection disabled") } + + var sdl []string + + for _, src := range sources { + if src.BuiltIn { + continue + } + sdl = append(sdl, src.Input) + } + return fedruntime.Service{ - SDL: `type Query { - me: User -} -type User @key(fields: "id") { - id: ID! - username: String! -} -`, + SDL: strings.Join(sdl, "\n"), }, nil } diff --git a/example/federation/accounts/graph/generated/generated.go b/example/federation/accounts/graph/generated/generated.go index 61303b8d877..dd379debd27 100644 --- a/example/federation/accounts/graph/generated/generated.go +++ b/example/federation/accounts/graph/generated/generated.go @@ -195,32 +195,47 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `directive @extends on OBJECT -directive @external on FIELD_DEFINITION -directive @key(fields: _FieldSet!) on OBJECT | INTERFACE -directive @provides(fields: _FieldSet!) on FIELD_DEFINITION -directive @requires(fields: _FieldSet!) on FIELD_DEFINITION -type Query { - me: User - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! +var sources = []*ast.Source{ + &ast.Source{Name: "graph/schema.graphqls", Input: `extend type Query { + me: User } + type User @key(fields: "id") { - id: ID! - username: String! + id: ID! + username: String! } +`, BuiltIn: false}, + &ast.Source{Name: "federation/directives.graphql", Input: ` scalar _Any -""" -A union unifies all @entity types (TODO: interfaces) -""" -union _Entity = User scalar _FieldSet + +directive @external on FIELD_DEFINITION +directive @requires(fields: _FieldSet!) on FIELD_DEFINITION +directive @provides(fields: _FieldSet!) on FIELD_DEFINITION +directive @key(fields: _FieldSet!) on OBJECT | INTERFACE +directive @extends on OBJECT +`, BuiltIn: true}, + &ast.Source{Name: "federation/entity.graphql", Input: ` +# a union of all types that use the @key directive +union _Entity = User + +# fake type to build resolver interfaces for users to implement +type Entity { + findUserByID(id: ID!,): User! + +} + type _Service { - sdl: String! + sdl: String } -`}, -) + +extend type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} +`, BuiltIn: true}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** @@ -612,14 +627,11 @@ func (ec *executionContext) __Service_sdl(ctx context.Context, field graphql.Col return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } res := resTmp.(string) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalOString2string(ctx, field.Selections, res) } func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { @@ -1855,9 +1867,6 @@ func (ec *executionContext) __Service(ctx context.Context, sel ast.SelectionSet, out.Values[i] = graphql.MarshalString("_Service") case "sdl": out.Values[i] = ec.__Service_sdl(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/example/federation/products/graph/generated/federation.go b/example/federation/products/graph/generated/federation.go index 72002a09219..6db7e70cd84 100644 --- a/example/federation/products/graph/generated/federation.go +++ b/example/federation/products/graph/generated/federation.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "strings" "github.com/99designs/gqlgen/plugin/federation/fedruntime" ) @@ -14,16 +15,18 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. if ec.DisableIntrospection { return fedruntime.Service{}, errors.New("federated introspection disabled") } + + var sdl []string + + for _, src := range sources { + if src.BuiltIn { + continue + } + sdl = append(sdl, src.Input) + } + return fedruntime.Service{ - SDL: `type Product @key(fields: "upc") { - upc: String! - name: String! - price: Int! -} -type Query { - topProducts(first: Int = 5): [Product] -} -`, + SDL: strings.Join(sdl, "\n"), }, nil } diff --git a/example/federation/products/graph/generated/generated.go b/example/federation/products/graph/generated/generated.go index 81b59f1a622..4b71ac3c01c 100644 --- a/example/federation/products/graph/generated/generated.go +++ b/example/federation/products/graph/generated/generated.go @@ -208,33 +208,48 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `directive @extends on OBJECT -directive @external on FIELD_DEFINITION -directive @key(fields: _FieldSet!) on OBJECT | INTERFACE -directive @provides(fields: _FieldSet!) on FIELD_DEFINITION -directive @requires(fields: _FieldSet!) on FIELD_DEFINITION -type Product @key(fields: "upc") { - upc: String! - name: String! - price: Int! +var sources = []*ast.Source{ + &ast.Source{Name: "graph/schema.graphqls", Input: `extend type Query { + topProducts(first: Int = 5): [Product] } -type Query { - topProducts(first: Int = 5): [Product] - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! + +type Product @key(fields: "upc") { + upc: String! + name: String! + price: Int! } +`, BuiltIn: false}, + &ast.Source{Name: "federation/directives.graphql", Input: ` scalar _Any -""" -A union unifies all @entity types (TODO: interfaces) -""" -union _Entity = Product scalar _FieldSet + +directive @external on FIELD_DEFINITION +directive @requires(fields: _FieldSet!) on FIELD_DEFINITION +directive @provides(fields: _FieldSet!) on FIELD_DEFINITION +directive @key(fields: _FieldSet!) on OBJECT | INTERFACE +directive @extends on OBJECT +`, BuiltIn: true}, + &ast.Source{Name: "federation/entity.graphql", Input: ` +# a union of all types that use the @key directive +union _Entity = Product + +# fake type to build resolver interfaces for users to implement +type Entity { + findProductByUpc(upc: String!,): Product! + +} + type _Service { - sdl: String! + sdl: String } -`}, -) + +extend type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} +`, BuiltIn: true}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** @@ -681,14 +696,11 @@ func (ec *executionContext) __Service_sdl(ctx context.Context, field graphql.Col return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } res := resTmp.(string) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalOString2string(ctx, field.Selections, res) } func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { @@ -1929,9 +1941,6 @@ func (ec *executionContext) __Service(ctx context.Context, sel ast.SelectionSet, out.Values[i] = graphql.MarshalString("_Service") case "sdl": out.Values[i] = ec.__Service_sdl(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/example/federation/reviews/graph/generated/federation.go b/example/federation/reviews/graph/generated/federation.go index 5b831eca02d..60a34909578 100644 --- a/example/federation/reviews/graph/generated/federation.go +++ b/example/federation/reviews/graph/generated/federation.go @@ -6,6 +6,7 @@ import ( "context" "errors" "fmt" + "strings" "github.com/99designs/gqlgen/plugin/federation/fedruntime" ) @@ -14,21 +15,18 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. if ec.DisableIntrospection { return fedruntime.Service{}, errors.New("federated introspection disabled") } + + var sdl []string + + for _, src := range sources { + if src.BuiltIn { + continue + } + sdl = append(sdl, src.Input) + } + return fedruntime.Service{ - SDL: `type Product @extends @key(fields: "upc") { - upc: String! @external - reviews: [Review] -} -type Review { - body: String! - author: User! @provides(fields: "username") - product: Product! -} -type User @extends @key(fields: "id") { - id: ID! @external - reviews: [Review] -} -`, + SDL: strings.Join(sdl, "\n"), }, nil } diff --git a/example/federation/reviews/graph/generated/generated.go b/example/federation/reviews/graph/generated/generated.go index 2c5a294516d..2c4fd44a989 100644 --- a/example/federation/reviews/graph/generated/generated.go +++ b/example/federation/reviews/graph/generated/generated.go @@ -251,40 +251,55 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `directive @extends on OBJECT -directive @external on FIELD_DEFINITION -directive @key(fields: _FieldSet!) on OBJECT | INTERFACE -directive @provides(fields: _FieldSet!) on FIELD_DEFINITION -directive @requires(fields: _FieldSet!) on FIELD_DEFINITION -type Product @extends @key(fields: "upc") { - upc: String! @external - reviews: [Review] +var sources = []*ast.Source{ + &ast.Source{Name: "graph/schema.graphqls", Input: `type Review { + body: String! + author: User! @provides(fields: "username") + product: Product! } -type Query { - _entities(representations: [_Any!]!): [_Entity]! - _service: _Service! -} -type Review { - body: String! - author: User! @provides(fields: "username") - product: Product! + +extend type User @key(fields: "id") { + id: ID! @external + reviews: [Review] } -type User @extends @key(fields: "id") { - id: ID! @external - reviews: [Review] + +extend type Product @key(fields: "upc") { + upc: String! @external + reviews: [Review] } +`, BuiltIn: false}, + &ast.Source{Name: "federation/directives.graphql", Input: ` scalar _Any -""" -A union unifies all @entity types (TODO: interfaces) -""" -union _Entity = Product | User scalar _FieldSet + +directive @external on FIELD_DEFINITION +directive @requires(fields: _FieldSet!) on FIELD_DEFINITION +directive @provides(fields: _FieldSet!) on FIELD_DEFINITION +directive @key(fields: _FieldSet!) on OBJECT | INTERFACE +directive @extends on OBJECT +`, BuiltIn: true}, + &ast.Source{Name: "federation/entity.graphql", Input: ` +# a union of all types that use the @key directive +union _Entity = Product | User + +# fake type to build resolver interfaces for users to implement +type Entity { + findProductByUpc(upc: String!,): Product! + findUserByID(id: ID!,): User! + +} + type _Service { - sdl: String! + sdl: String } -`}, -) + +extend type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} +`, BuiltIn: true}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** @@ -864,14 +879,11 @@ func (ec *executionContext) __Service_sdl(ctx context.Context, field graphql.Col return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } res := resTmp.(string) fc.Result = res - return ec.marshalNString2string(ctx, field.Selections, res) + return ec.marshalOString2string(ctx, field.Selections, res) } func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) { @@ -2198,9 +2210,6 @@ func (ec *executionContext) __Service(ctx context.Context, sel ast.SelectionSet, out.Values[i] = graphql.MarshalString("_Service") case "sdl": out.Values[i] = ec.__Service_sdl(ctx, field, obj) - if out.Values[i] == graphql.Null { - invalids++ - } default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/example/federation/reviews/graph/schema.graphqls b/example/federation/reviews/graph/schema.graphqls index 8cd78ecf39d..72cfcf3a3c8 100644 --- a/example/federation/reviews/graph/schema.graphqls +++ b/example/federation/reviews/graph/schema.graphqls @@ -4,12 +4,12 @@ type Review { product: Product! } -type User @extends @key(fields: "id") { +extend type User @key(fields: "id") { id: ID! @external reviews: [Review] } -type Product @extends @key(fields: "upc") { +extend type Product @key(fields: "upc") { upc: String! @external reviews: [Review] } diff --git a/example/fileupload/generated.go b/example/fileupload/generated.go index 8317eb3398c..ccc53004cec 100644 --- a/example/fileupload/generated.go +++ b/example/fileupload/generated.go @@ -225,43 +225,39 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `""" -The ` + "`" + `File` + "`" + ` type, represents the response of uploading a file. -""" +var sources = []*ast.Source{ + &ast.Source{Name: "schema.graphql", Input: `"The ` + "`" + `Upload` + "`" + ` scalar type represents a multipart file upload." +scalar Upload + +"The ` + "`" + `File` + "`" + ` type, represents the response of uploading a file." type File { - id: Int! - name: String! - content: String! + id: Int! + name: String! + content: String! } -""" -The ` + "`" + `Mutation` + "`" + ` type, represents all updates we can make to our data. -""" -type Mutation { - singleUpload(file: Upload!): File! - singleUploadWithPayload(req: UploadFile!): File! - multipleUpload(files: [Upload!]!): [File!]! - multipleUploadWithPayload(req: [UploadFile!]!): [File!]! -} -""" -The ` + "`" + `Query` + "`" + ` type, represents all of the entry points into our object graph. -""" + +"The ` + "`" + `UploadFile` + "`" + ` type, represents the request for uploading a file with certain payload." +input UploadFile { + id: Int! + file: Upload! +} + +"The ` + "`" + `Query` + "`" + ` type, represents all of the entry points into our object graph." type Query { - empty: String! + empty: String! } -""" -The ` + "`" + `Upload` + "`" + ` scalar type represents a multipart file upload. -""" -scalar Upload -""" -The ` + "`" + `UploadFile` + "`" + ` type, represents the request for uploading a file with certain payload. -""" -input UploadFile { - id: Int! - file: Upload! + +"The ` + "`" + `Mutation` + "`" + ` type, represents all updates we can make to our data." +type Mutation { + singleUpload(file: Upload!): File! + singleUploadWithPayload(req: UploadFile!): File! + multipleUpload(files: [Upload!]!): [File!]! + multipleUploadWithPayload(req: [UploadFile!]!): [File!]! } -`}, -) + +`, BuiltIn: false}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/example/scalars/generated.go b/example/scalars/generated.go index 4ad054ad0ec..764f63314c8 100644 --- a/example/scalars/generated.go +++ b/example/scalars/generated.go @@ -234,40 +234,46 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `type Address { - id: ID! - location: Point +var sources = []*ast.Source{ + &ast.Source{Name: "schema.graphql", Input: `type Query { + user(id: ID!): User + search(input: SearchArgs = {location: "37,144", isBanned: false}): [User!]! } -scalar Banned -scalar Point -type Query { - user(id: ID!): User - search(input: SearchArgs = {location:"37,144",isBanned:false}): [User!]! + +type User { + id: ID! + name: String! + created: Timestamp + isBanned: Banned! + primitiveResolver: String! + customResolver: Point! + address: Address + tier: Tier } + +type Address { + id: ID! + location: Point +} + input SearchArgs { - location: Point - createdAfter: Timestamp - isBanned: Banned + location: Point + createdAfter: Timestamp + isBanned: Banned # TODO: This can be a Boolean again once multiple backing types are allowed } + enum Tier { - A - B - C + A + B + C } + scalar Timestamp -type User { - id: ID! - name: String! - created: Timestamp - isBanned: Banned! - primitiveResolver: String! - customResolver: Point! - address: Address - tier: Tier -} -`}, -) +scalar Point +scalar Banned +`, BuiltIn: false}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/example/selection/generated.go b/example/selection/generated.go index 554bcc66736..29df5e30486 100644 --- a/example/selection/generated.go +++ b/example/selection/generated.go @@ -192,29 +192,34 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( +var sources = []*ast.Source{ &ast.Source{Name: "schema.graphql", Input: `interface Event { - selection: [String!] - collected: [String!] -} -type Like implements Event { - reaction: String! - sent: Time! - selection: [String!] - collected: [String!] + selection: [String!] + collected: [String!] } + type Post implements Event { - message: String! - sent: Time! - selection: [String!] - collected: [String!] + message: String! + sent: Time! + selection: [String!] + collected: [String!] +} + +type Like implements Event { + reaction: String! + sent: Time! + selection: [String!] + collected: [String!] } + type Query { - events: [Event!] + events: [Event!] } + scalar Time -`}, -) +`, BuiltIn: false}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/example/starwars/generated/exec.go b/example/starwars/generated/exec.go index 8c43302f3c8..c51b74bd58e 100644 --- a/example/starwars/generated/exec.go +++ b/example/starwars/generated/exec.go @@ -549,88 +549,141 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `interface Character { - id: ID! - name: String! - friends: [Character!] - friendsConnection(first: Int, after: ID): FriendsConnection! - appearsIn: [Episode!]! -} -type Droid implements Character { - id: ID! - name: String! - friends: [Character!] - friendsConnection(first: Int, after: ID): FriendsConnection! - appearsIn: [Episode!]! - primaryFunction: String +var sources = []*ast.Source{ + &ast.Source{Name: "schema.graphql", Input: `# The query type, represents all of the entry points into our object graph +type Query { + hero(episode: Episode = NEWHOPE): Character + reviews(episode: Episode!, since: Time): [Review!]! + search(text: String!): [SearchResult!]! + character(id: ID!): Character + droid(id: ID!): Droid + human(id: ID!): Human + starship(id: ID!): Starship +} +# The mutation type, represents all updates we can make to our data +type Mutation { + createReview(episode: Episode!, review: ReviewInput!): Review } +# The episodes in the Star Wars trilogy enum Episode { - NEWHOPE - EMPIRE - JEDI + # Star Wars Episode IV: A New Hope, released in 1977. + NEWHOPE + # Star Wars Episode V: The Empire Strikes Back, released in 1980. + EMPIRE + # Star Wars Episode VI: Return of the Jedi, released in 1983. + JEDI +} +# A character from the Star Wars universe +interface Character { + # The ID of the character + id: ID! + # The name of the character + name: String! + # The friends of the character, or an empty list if they have none + friends: [Character!] + # The friends of the character exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + # The movies this character appears in + appearsIn: [Episode!]! +} +# Units of height +enum LengthUnit { + # The standard unit around the world + METER + # Primarily used in the United States + FOOT } +# A humanoid creature from the Star Wars universe +type Human implements Character { + # The ID of the human + id: ID! + # What this human calls themselves + name: String! + # Height in the preferred unit, default is meters + height(unit: LengthUnit = METER): Float! + # Mass in kilograms, or null if unknown + mass: Float + # This human's friends, or an empty list if they have none + friends: [Character!] + # The friends of the human exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + # The movies this human appears in + appearsIn: [Episode!]! + # A list of starships this person has piloted, or an empty list if none + starships: [Starship!] +} +# An autonomous mechanical character in the Star Wars universe +type Droid implements Character { + # The ID of the droid + id: ID! + # What others call this droid + name: String! + # This droid's friends, or an empty list if they have none + friends: [Character!] + # The friends of the droid exposed as a connection with edges + friendsConnection(first: Int, after: ID): FriendsConnection! + # The movies this droid appears in + appearsIn: [Episode!]! + # This droid's primary function + primaryFunction: String +} +# A connection object for a character's friends type FriendsConnection { - totalCount: Int! - edges: [FriendsEdge!] - friends: [Character!] - pageInfo: PageInfo! -} + # The total number of friends + totalCount: Int! + # The edges for each of the character's friends. + edges: [FriendsEdge!] + # A list of the friends, as a convenience when edges are not needed. + friends: [Character!] + # Information for paginating this connection + pageInfo: PageInfo! +} +# An edge object for a character's friends type FriendsEdge { - cursor: ID! - node: Character -} -type Human implements Character { - id: ID! - name: String! - height(unit: LengthUnit = METER): Float! - mass: Float - friends: [Character!] - friendsConnection(first: Int, after: ID): FriendsConnection! - appearsIn: [Episode!]! - starships: [Starship!] -} -enum LengthUnit { - METER - FOOT -} -type Mutation { - createReview(episode: Episode!, review: ReviewInput!): Review + # A cursor used for pagination + cursor: ID! + # The character represented by this friendship edge + node: Character } +# Information for paginating this connection type PageInfo { - startCursor: ID! - endCursor: ID! - hasNextPage: Boolean! -} -type Query { - hero(episode: Episode = NEWHOPE): Character - reviews(episode: Episode!, since: Time): [Review!]! - search(text: String!): [SearchResult!]! - character(id: ID!): Character - droid(id: ID!): Droid - human(id: ID!): Human - starship(id: ID!): Starship + startCursor: ID! + endCursor: ID! + hasNextPage: Boolean! } +# Represents a review for a movie type Review { - stars: Int! - commentary: String - time: Time -} + # The number of stars this review gave, 1-5 + stars: Int! + # Comment about the movie + commentary: String + # when the review was posted + time: Time +} +# The input object sent when someone is creating a new review input ReviewInput { - stars: Int! - commentary: String - time: Time + # 0-5 stars + stars: Int! + # Comment about the movie, optional + commentary: String + # when the review was posted + time: Time } -union SearchResult = Human | Droid | Starship type Starship { - id: ID! - name: String! - length(unit: LengthUnit = METER): Float! - history: [[Int!]!]! + # The ID of the starship + id: ID! + # The name of the starship + name: String! + # Length of the starship, along the longest axis + length(unit: LengthUnit = METER): Float! + # coordinates tracking this ship + history: [[Int!]!]! } +union SearchResult = Human | Droid | Starship scalar Time -`}, -) +`, BuiltIn: false}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/example/todo/generated.go b/example/todo/generated.go index 6e50484720e..e3c2a40b137 100644 --- a/example/todo/generated.go +++ b/example/todo/generated.go @@ -226,50 +226,50 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( +var sources = []*ast.Source{ &ast.Source{Name: "schema.graphql", Input: `schema { - query: MyQuery - mutation: MyMutation -} -""" -Prevents access to a field if the user doesnt have the matching role -""" -directive @hasRole(role: Role!) on FIELD_DEFINITION -directive @user(id: ID!) on MUTATION | QUERY | FIELD -scalar Map -type MyMutation { - createTodo(todo: TodoInput!): Todo! - updateTodo(id: ID!, changes: Map!): Todo + query: MyQuery + mutation: MyMutation } + type MyQuery { - todo(id: ID!): Todo - lastTodo: Todo - todos: [Todo!]! + todo(id: ID!): Todo + lastTodo: Todo + todos: [Todo!]! } -enum Role { - ADMIN - OWNER + +type MyMutation { + createTodo(todo: TodoInput!): Todo! + updateTodo(id: ID!, changes: Map!): Todo } + type Todo { - id: ID! - text: String! - done: Boolean! @hasRole(role: OWNER) + id: ID! + text: String! + done: Boolean! @hasRole(role: OWNER) # only the owner can see if a todo is done } -""" -Passed to createTodo to create a new todo -""" + +"Passed to createTodo to create a new todo" input TodoInput { - """ - The body text - """ - text: String! - """ - Is it done already? - """ - done: Boolean -} -`}, -) + "The body text" + text: String! + "Is it done already?" + done: Boolean +} + +scalar Map + +"Prevents access to a field if the user doesnt have the matching role" +directive @hasRole(role: Role!) on FIELD_DEFINITION +directive @user(id: ID!) on MUTATION | QUERY | FIELD + +enum Role { + ADMIN + OWNER +} +`, BuiltIn: false}, +} +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/example/type-system-extension/generated.go b/example/type-system-extension/generated.go index dc4a99bfe21..d6e052d240f 100644 --- a/example/type-system-extension/generated.go +++ b/example/type-system-extension/generated.go @@ -212,44 +212,84 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `schema { - query: MyQuery - mutation: MyMutation -} -directive @enumLogging on ENUM -directive @fieldLogging on FIELD_DEFINITION -directive @inputLogging on INPUT_OBJECT -directive @interfaceLogging on INTERFACE -directive @objectLogging on OBJECT -directive @scalarLogging on SCALAR -directive @unionLogging on UNION -union Data @unionLogging = Todo +var sources = []*ast.Source{ + &ast.Source{Name: "schemas/enum-extension.graphql", Input: `directive @enumLogging on ENUM + +extend enum State @enumLogging +`, BuiltIn: false}, + &ast.Source{Name: "schemas/input-object-extension.graphql", Input: `directive @inputLogging on INPUT_OBJECT + +extend input TodoInput @inputLogging +`, BuiltIn: false}, + &ast.Source{Name: "schemas/interface-extension.graphql", Input: `directive @interfaceLogging on INTERFACE + +extend interface Node @interfaceLogging +`, BuiltIn: false}, + &ast.Source{Name: "schemas/object-extension.graphql", Input: `directive @objectLogging on OBJECT + +extend type Todo @objectLogging +`, BuiltIn: false}, + &ast.Source{Name: "schemas/scalar-extension.graphql", Input: `directive @scalarLogging on SCALAR + +extend scalar ID @scalarLogging +`, BuiltIn: false}, + &ast.Source{Name: "schemas/schema-extension.graphql", Input: `extend schema { + mutation: MyMutation +} + +extend type MyQuery { + todo(id: ID!): Todo +} + type MyMutation { - createTodo(todo: TodoInput!): Todo! + createTodo(todo: TodoInput!): Todo! } -type MyQuery { - todos: [Todo!]! - todo(id: ID!): Todo + +input TodoInput { + text: String! +} +`, BuiltIn: false}, + &ast.Source{Name: "schemas/schema.graphql", Input: `# GraphQL schema example +# +# https://gqlgen.com/getting-started/ + +schema { + query: MyQuery +} + +interface Node { + id: ID! } -interface Node @interfaceLogging { - id: ID! + +type Todo implements Node { + id: ID! + text: String! + state: State! +} + +type MyQuery { + todos: [Todo!]! } -enum State @enumLogging { - NOT_YET - DONE + +union Data = Todo + +enum State { + NOT_YET + DONE } -type Todo implements Node @objectLogging { - id: ID! - text: String! - state: State! - verified: Boolean! @fieldLogging +`, BuiltIn: false}, + &ast.Source{Name: "schemas/type-extension.graphql", Input: `directive @fieldLogging on FIELD_DEFINITION + +extend type Todo { + verified: Boolean! @fieldLogging } -input TodoInput @inputLogging { - text: String! +`, BuiltIn: false}, + &ast.Source{Name: "schemas/union-extension.graphql", Input: `directive @unionLogging on UNION + +extend union Data @unionLogging +`, BuiltIn: false}, } -`}, -) +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/go.mod b/go.mod index 42a32211359..7726e1ddfe5 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/stretchr/testify v1.3.0 github.com/urfave/cli v1.20.0 github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e - github.com/vektah/gqlparser v1.2.2-0.20200206213904-3eaa5ac7807c + github.com/vektah/gqlparser v1.3.1 golang.org/x/tools v0.0.0-20200114235610-7ae403b6b589 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.2.2 diff --git a/go.sum b/go.sum index 25002a9a0fc..06f3b01ccf0 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,8 @@ github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= -github.com/vektah/gqlparser v1.2.2-0.20200206213904-3eaa5ac7807c h1:Qzywl6AFiBv93OEbhLybAOq1qW6DcCRtK9ZmY+rJnGs= -github.com/vektah/gqlparser v1.2.2-0.20200206213904-3eaa5ac7807c/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= +github.com/vektah/gqlparser v1.3.1 h1:8b0IcD3qZKWJQHSzynbDlrtP3IxVydZ2DZepCGofqfU= +github.com/vektah/gqlparser v1.3.1/go.mod h1:bkVf0FX+Stjg/MHnm8mEyubuaArhNEqfQhF+OTiAL74= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= diff --git a/integration/generated.go b/integration/generated.go index b5d11947976..e8516821fe9 100644 --- a/integration/generated.go +++ b/integration/generated.go @@ -251,50 +251,61 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er return introspection.WrapTypeFromDef(parsedSchema, parsedSchema.Types[name]), nil } -var parsedSchema = gqlparser.MustLoadSchema( - &ast.Source{Name: "schema.graphql", Input: `""" -This directive does magical things -""" +var sources = []*ast.Source{ + &ast.Source{Name: "schema.graphql", Input: `"This directive does magical things" directive @magic(kind: Int) on FIELD_DEFINITION + +type Element { + child: Element! + error: Boolean! + mismatched: [Boolean!] +} + enum DATE_FILTER_OP { - EQ - NEQ - GT - GTE - LT - LTE + # multi + # line + # comment + EQ + NEQ + GT + GTE + LT + LTE } + input DateFilter { - value: String! - timezone: String = "UTC" - op: DATE_FILTER_OP = EQ + value: String! + timezone: String = "UTC" + op: DATE_FILTER_OP = EQ } -type Element { - child: Element! - error: Boolean! - mismatched: [Boolean!] -} -enum ErrorType { - CUSTOM - NORMAL + +type Viewer { + user: User } + type Query { - path: [Element] - date(filter: DateFilter!): Boolean! - viewer: Viewer - jsonEncoding: String! - error(type: ErrorType = NORMAL): Boolean! - complexity(value: Int!): Boolean! + path: [Element] + date(filter: DateFilter!): Boolean! + viewer: Viewer + jsonEncoding: String! + error(type: ErrorType = NORMAL): Boolean! + complexity(value: Int!): Boolean! +} + +enum ErrorType { + CUSTOM + NORMAL } -type User { - name: String! - likes: [String!]! + +# this is a comment with a ` + "`" + `backtick` + "`" + ` +`, BuiltIn: false}, + &ast.Source{Name: "user.graphql", Input: `type User { + name: String! + likes: [String!]! } -type Viewer { - user: User +`, BuiltIn: false}, } -`}, -) +var parsedSchema = gqlparser.MustLoadSchema(sources...) // endregion ************************** generated!.gotpl ************************** diff --git a/plugin/federation/federation.go b/plugin/federation/federation.go index 63d5fe4ea94..58f503494c2 100644 --- a/plugin/federation/federation.go +++ b/plugin/federation/federation.go @@ -1,17 +1,11 @@ package federation import ( - "bytes" "fmt" - "io/ioutil" - "os" - "path/filepath" "sort" "strings" - "github.com/vektah/gqlparser" "github.com/vektah/gqlparser/ast" - "github.com/vektah/gqlparser/formatter" "github.com/99designs/gqlgen/codegen" "github.com/99designs/gqlgen/codegen/config" @@ -20,7 +14,6 @@ import ( ) type federation struct { - SDL string Entities []*Entity } @@ -37,18 +30,6 @@ func (f *federation) Name() string { // MutateConfig mutates the configuration func (f *federation) MutateConfig(cfg *config.Config) error { - entityFields := map[string]config.TypeMapField{} - for _, e := range f.Entities { - entityFields[e.ResolverName] = config.TypeMapField{Resolver: true} - for _, r := range e.Requires { - if cfg.Models[e.Def.Name].Fields == nil { - model := cfg.Models[e.Def.Name] - model.Fields = map[string]config.TypeMapField{} - cfg.Models[e.Def.Name] = model - } - cfg.Models[e.Def.Name].Fields[r.Name] = config.TypeMapField{Resolver: true} - } - } builtins := config.TypeMap{ "_Service": { Model: config.StringList{ @@ -84,125 +65,71 @@ func (f *federation) MutateConfig(cfg *config.Config) error { return nil } -// InjectSources creates a GraphQL Entity type with all -// the fields that had the @key directive -func (f *federation) InjectSources(cfg *config.Config) { - cfg.AdditionalSources = append(cfg.AdditionalSources, f.getSource(false)) +func (f *federation) InjectSourceEarly() *ast.Source { + return &ast.Source{ + Name: "federation/directives.graphql", + Input: ` +scalar _Any +scalar _FieldSet - f.setEntities(cfg) - if len(f.Entities) == 0 { - // It's unusual for a service not to have any entities, but - // possible if it only exports top-level queries and mutations. - return +directive @external on FIELD_DEFINITION +directive @requires(fields: _FieldSet!) on FIELD_DEFINITION +directive @provides(fields: _FieldSet!) on FIELD_DEFINITION +directive @key(fields: _FieldSet!) on OBJECT | INTERFACE +directive @extends on OBJECT +`, + BuiltIn: true, } +} + +// InjectSources creates a GraphQL Entity type with all +// the fields that had the @key directive +func (f *federation) InjectSourceLate(schema *ast.Schema) *ast.Source { + f.setEntities(schema) + + entities := "" + resolvers := "" + for i, e := range f.Entities { + if i != 0 { + entities += " | " + } + entities += e.Name - s := "type Entity {\n" - for _, e := range f.Entities { resolverArgs := "" for _, field := range e.KeyFields { resolverArgs += fmt.Sprintf("%s: %s,", field.Field.Name, field.Field.Type.String()) } - s += fmt.Sprintf("\t%s(%s): %s!\n", e.ResolverName, resolverArgs, e.Def.Name) - } - s += "}" - cfg.AdditionalSources = append(cfg.AdditionalSources, &ast.Source{Name: "entity.graphql", Input: s, BuiltIn: true}) -} + resolvers += fmt.Sprintf("\t%s(%s): %s!\n", e.ResolverName, resolverArgs, e.Def.Name) -// ensureQuery ensures that a "Query" node exists on the schema. -func ensureQuery(s *ast.Schema) { - if s.Query == nil { - s.Query = &ast.Definition{ - Kind: ast.Object, - Name: "Query", - } - s.Types["Query"] = s.Query } -} -// addEntityToSchema adds the _Entity Union and _entities query to schema. -// This is part of MutateSchema. -func (f *federation) addEntityToSchema(s *ast.Schema) { - // --- Set _Entity Union --- - union := &ast.Definition{ - Name: "_Entity", - Kind: ast.Union, - Description: "A union unifies all @entity types (TODO: interfaces)", - Types: []string{}, - } - for _, ent := range f.Entities { - union.Types = append(union.Types, ent.Def.Name) - s.AddPossibleType("_Entity", ent.Def) - s.AddImplements(ent.Def.Name, union) - } - s.Types[union.Name] = union - - // --- Set _entities query --- - fieldDef := &ast.FieldDefinition{ - Name: "_entities", - Type: ast.NonNullListType(ast.NamedType("_Entity", nil), nil), - Arguments: ast.ArgumentDefinitionList{ - { - Name: "representations", - Type: ast.NonNullListType(ast.NonNullNamedType("_Any", nil), nil), - }, - }, - } - ensureQuery(s) - s.Query.Fields = append(s.Query.Fields, fieldDef) -} - -// addServiceToSchema adds the _Service type and _service query to schema. -// This is part of MutateSchema. -func (f *federation) addServiceToSchema(s *ast.Schema) { - typeDef := &ast.Definition{ - Kind: ast.Object, - Name: "_Service", - Fields: ast.FieldList{ - &ast.FieldDefinition{ - Name: "sdl", - Type: ast.NonNullNamedType("String", nil), - }, - }, + if len(f.Entities) == 0 { + // It's unusual for a service not to have any entities, but + // possible if it only exports top-level queries and mutations. + return nil } - s.Types[typeDef.Name] = typeDef - // --- set _service query --- - _serviceDef := &ast.FieldDefinition{ - Name: "_service", - Type: ast.NonNullNamedType("_Service", nil), - } - ensureQuery(s) - s.Query.Fields = append(s.Query.Fields, _serviceDef) + return &ast.Source{ + Name: "federation/entity.graphql", + BuiltIn: true, + Input: ` +# a union of all types that use the @key directive +union _Entity = ` + entities + ` + +# fake type to build resolver interfaces for users to implement +type Entity { + ` + resolvers + ` } -// MutateSchema creates types and query declarations -// that are required by the federation spec. -func (f *federation) MutateSchema(s *ast.Schema) error { - // It's unusual for a service not to have any entities, but - // possible if it only exports top-level queries and mutations. - if len(f.Entities) > 0 { - f.addEntityToSchema(s) - } - f.addServiceToSchema(s) - return nil +type _Service { + sdl: String } -func (f *federation) getSource(builtin bool) *ast.Source { - return &ast.Source{ - Name: "federation.graphql", - Input: `# Declarations as required by the federation spec -# See: https://www.apollographql.com/docs/apollo-server/federation/federation-spec/ - -scalar _Any -scalar _FieldSet - -directive @external on FIELD_DEFINITION -directive @requires(fields: _FieldSet!) on FIELD_DEFINITION -directive @provides(fields: _FieldSet!) on FIELD_DEFINITION -directive @key(fields: _FieldSet!) on OBJECT | INTERFACE -directive @extends on OBJECT +extend type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! +} `, - BuiltIn: builtin, } } @@ -236,11 +163,6 @@ type RequireField struct { } func (f *federation) GenerateCode(data *codegen.Data) error { - sdl, err := f.getSDL(data.Config) - if err != nil { - return err - } - f.SDL = sdl if len(f.Entities) > 0 { data.Objects.ByName("Entity").Root = true for _, e := range f.Entities { @@ -283,14 +205,8 @@ func (f *federation) getKeyField(keyFields []*KeyField, fieldName string) *KeyFi return nil } -func (f *federation) setEntities(cfg *config.Config) { - // crazy hack to get our injected code in so everything compiles, so we can generate the entity map - // so we can reload the full schema. - err := cfg.LoadSchema() - if err != nil { - panic(err) - } - for _, schemaType := range cfg.Schema.Types { +func (f *federation) setEntities(schema *ast.Schema) { + for _, schemaType := range schema.Types { if schemaType.Kind == ast.Object { dir := schemaType.Directives.ForName("key") // TODO: interfaces if dir != nil { @@ -351,25 +267,3 @@ func (f *federation) setEntities(cfg *config.Config) { return f.Entities[i].Name < f.Entities[j].Name }) } - -func (f *federation) getSDL(c *config.Config) (string, error) { - sources := []*ast.Source{f.getSource(true)} - for _, filename := range c.SchemaFilename { - filename = filepath.ToSlash(filename) - var err error - var schemaRaw []byte - schemaRaw, err = ioutil.ReadFile(filename) - if err != nil { - fmt.Fprintln(os.Stderr, "unable to open schema: "+err.Error()) - os.Exit(1) - } - sources = append(sources, &ast.Source{Name: filename, Input: string(schemaRaw)}) - } - schema, err := gqlparser.LoadSchema(sources...) - if err != nil { - return "", err - } - var buf bytes.Buffer - formatter.NewFormatter(&buf).FormatSchema(schema) - return buf.String(), nil -} diff --git a/plugin/federation/federation.gotpl b/plugin/federation/federation.gotpl index 9fab3f04f0f..2661495a388 100644 --- a/plugin/federation/federation.gotpl +++ b/plugin/federation/federation.gotpl @@ -1,6 +1,7 @@ {{ reserveImport "context" }} {{ reserveImport "errors" }} {{ reserveImport "fmt" }} +{{ reserveImport "strings" }} {{ reserveImport "github.com/99designs/gqlgen/plugin/federation/fedruntime" }} @@ -8,8 +9,18 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. if ec.DisableIntrospection { return fedruntime.Service{}, errors.New("federated introspection disabled") } + + var sdl []string + + for _, src := range sources { + if src.BuiltIn { + continue + } + sdl = append(sdl, src.Input) + } + return fedruntime.Service{ - SDL: `{{.SDL}}`, + SDL: strings.Join(sdl, "\n"), }, nil } diff --git a/plugin/federation/federation_test.go b/plugin/federation/federation_test.go index 0fa553fb126..f6a164e89ed 100644 --- a/plugin/federation/federation_test.go +++ b/plugin/federation/federation_test.go @@ -5,84 +5,42 @@ import ( "github.com/99designs/gqlgen/codegen/config" "github.com/stretchr/testify/require" - "github.com/vektah/gqlparser" - "github.com/vektah/gqlparser/ast" ) -func TestInjectSources(t *testing.T) { - cfg, err := config.LoadConfig("test_data/gqlgen.yml") - require.NoError(t, err) - f := &federation{} - f.InjectSources(cfg) - if len(cfg.AdditionalSources) != 2 { - t.Fatalf("expected 2 additional sources but got %v", len(cfg.AdditionalSources)) - } -} +func TestWithEntities(t *testing.T) { + f, cfg := load(t, "test_data/gqlgen.yml") -func TestMutateSchema(t *testing.T) { - f := &federation{} + require.Equal(t, []string{"ExternalExtension", "Hello", "World"}, cfg.Schema.Types["_Entity"].Types) - schema, gqlErr := gqlparser.LoadSchema(&ast.Source{ - Name: "schema.graphql", - Input: `type Query { - hello: String! - world: String! - }`, - }) - if gqlErr != nil { - t.Fatal(gqlErr) - } + require.Equal(t, "findExternalExtensionByUpc", cfg.Schema.Types["Entity"].Fields[0].Name) + require.Equal(t, "findHelloByName", cfg.Schema.Types["Entity"].Fields[1].Name) + require.Equal(t, "findWorldByFooAndBar", cfg.Schema.Types["Entity"].Fields[2].Name) - err := f.MutateSchema(schema) - require.NoError(t, err) + require.NoError(t, f.MutateConfig(cfg)) } -func TestGetSDL(t *testing.T) { - cfg, err := config.LoadConfig("test_data/gqlgen.yml") - require.NoError(t, err) - f := &federation{} - _, err = f.getSDL(cfg) +func TestNoEntities(t *testing.T) { + f, cfg := load(t, "test_data/nokey.yml") + + err := f.MutateConfig(cfg) require.NoError(t, err) } -func TestMutateConfig(t *testing.T) { - cfg, err := config.LoadConfig("test_data/gqlgen.yml") +func load(t *testing.T, name string) (*federation, *config.Config) { + t.Helper() + + cfg, err := config.LoadConfig(name) require.NoError(t, err) f := &federation{} - f.InjectSources(cfg) - + cfg.Sources = append(cfg.Sources, f.InjectSourceEarly()) require.NoError(t, cfg.LoadSchema()) - require.NoError(t, f.MutateSchema(cfg.Schema)) - require.NoError(t, cfg.Init()) - require.NoError(t, f.MutateConfig(cfg)) -} - -func TestInjectSourcesNoKey(t *testing.T) { - cfg, err := config.LoadConfig("test_data/nokey.yml") - require.NoError(t, err) - f := &federation{} - f.InjectSources(cfg) - if len(cfg.AdditionalSources) != 1 { - t.Fatalf("expected an additional source but got %v", len(cfg.AdditionalSources)) + if src := f.InjectSourceLate(cfg.Schema); src != nil { + cfg.Sources = append(cfg.Sources, src) } -} - -func TestGetSDLNoKey(t *testing.T) { - cfg, err := config.LoadConfig("test_data/nokey.yml") - require.NoError(t, err) - f := &federation{} - _, err = f.getSDL(cfg) - require.NoError(t, err) -} + require.NoError(t, cfg.LoadSchema()) -func TestMutateConfigNoKey(t *testing.T) { - cfg, err := config.LoadConfig("test_data/nokey.yml") - require.NoError(t, err) require.NoError(t, cfg.Init()) - - f := &federation{} - err = f.MutateConfig(cfg) - require.NoError(t, err) + return f, cfg } diff --git a/plugin/federation/test_data/schema.graphql b/plugin/federation/test_data/schema.graphql index 55e04d341e5..a47f8b224aa 100644 --- a/plugin/federation/test_data/schema.graphql +++ b/plugin/federation/test_data/schema.graphql @@ -7,6 +7,11 @@ type World @key(fields: "foo bar") { bar: Int! } +extend type ExternalExtension @key(fields: "upc") { + upc: String! @external + reviews: [World] +} + type Query { hello: Hello! world: World! diff --git a/plugin/plugin.go b/plugin/plugin.go index 3a187453705..3ab5970c27d 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -20,10 +20,12 @@ type CodeGenerator interface { GenerateCode(cfg *codegen.Data) error } -type SourcesInjector interface { - InjectSources(cfg *config.Config) +// EarlySourceInjector is used to inject things that are required for user schema files to compile. +type EarlySourceInjector interface { + InjectSourceEarly() *ast.Source } -type SchemaMutator interface { - MutateSchema(s *ast.Schema) error +// LateSourceInjector is used to inject more sources, after we have loaded the users schema. +type LateSourceInjector interface { + InjectSourceLate(schema *ast.Schema) *ast.Source }