diff --git a/.github/workflows/check-integration b/.github/workflows/check-integration index 07e0e93c6f..c079cd663d 100755 --- a/.github/workflows/check-integration +++ b/.github/workflows/check-integration @@ -15,8 +15,6 @@ curl -s --connect-timeout 5 \ --retry-max-time 40 \ --retry-connrefused \ localhost:8080 > /dev/null -echo "### running integration spec" -npm run test echo "### validating introspected schema" @@ -26,3 +24,6 @@ if ! diff <(tail -n +3 src/generated/schema-expected.graphql) <(tail -n +3 src/g echo "The expected schema has changed, you need to update schema-expected.graphql with any expected changes" exit 1 fi + +echo "### running integration spec" +npm run test diff --git a/_examples/chat/server/server.go b/_examples/chat/server/server.go index e793528d4f..19e938f340 100644 --- a/_examples/chat/server/server.go +++ b/_examples/chat/server/server.go @@ -11,14 +11,15 @@ import ( "github.com/99designs/gqlgen/graphql/playground" - "github.com/99designs/gqlgen/_examples/chat" - "github.com/99designs/gqlgen/graphql/handler" "github.com/gorilla/websocket" "github.com/opentracing/opentracing-go" "github.com/rs/cors" "sourcegraph.com/sourcegraph/appdash" appdashtracer "sourcegraph.com/sourcegraph/appdash/opentracing" "sourcegraph.com/sourcegraph/appdash/traceapp" + + "github.com/99designs/gqlgen/_examples/chat" + "github.com/99designs/gqlgen/graphql/handler" ) func main() { diff --git a/graphql/introspection/introspection.go b/graphql/introspection/introspection.go index 8482d62a86..30c865cc0d 100644 --- a/graphql/introspection/introspection.go +++ b/graphql/introspection/introspection.go @@ -74,13 +74,15 @@ func (f *Field) IsDeprecated() bool { } func (f *Field) DeprecationReason() *string { - if f.deprecation == nil { + if f.deprecation == nil || !f.IsDeprecated() { return nil } reason := f.deprecation.Arguments.ForName("reason") + if reason == nil { - return nil + defaultReason := "No longer supported" + return &defaultReason } return &reason.Value.Raw diff --git a/integration/codegen.ts b/integration/codegen.ts index f191f72b39..389228c3cf 100644 --- a/integration/codegen.ts +++ b/integration/codegen.ts @@ -11,6 +11,9 @@ const config: CodegenConfig = { 'src/generated/schema-fetched.graphql': { plugins: ['schema-ast'], }, + 'src/generated/schema-introspection.json': { + plugins: ['introspection'], + } }, }; diff --git a/integration/package-lock.json b/integration/package-lock.json index 9f291a75ce..195a333e0c 100644 --- a/integration/package-lock.json +++ b/integration/package-lock.json @@ -11,6 +11,7 @@ "@apollo/client": "^3.7.14", "@graphql-codegen/cli": "^4.0.0", "@graphql-codegen/client-preset": "^4.0.0", + "@graphql-codegen/introspection": "^4.0.0", "@graphql-codegen/schema-ast": "^4.0.0", "graphql": "^16.6.0", "graphql-codegen": "^0.4.0", @@ -1557,6 +1558,20 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" } }, + "node_modules/@graphql-codegen/introspection": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@graphql-codegen/introspection/-/introspection-4.0.0.tgz", + "integrity": "sha512-t9g3AkK99dfHblMWtG4ynUM9+A7JrWq5110zSpNV2wlSnv0+bRKagDW8gozwgXfR5i1IIG8QDjJZ6VgXQVqCZw==", + "dev": true, + "dependencies": { + "@graphql-codegen/plugin-helpers": "^5.0.0", + "@graphql-codegen/visitor-plugin-common": "^4.0.0", + "tslib": "~2.5.0" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, "node_modules/@graphql-codegen/plugin-helpers": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@graphql-codegen/plugin-helpers/-/plugin-helpers-5.0.0.tgz", diff --git a/integration/package.json b/integration/package.json index b3b4e329c0..c2fde89609 100644 --- a/integration/package.json +++ b/integration/package.json @@ -9,15 +9,16 @@ }, "devDependencies": { "@apollo/client": "^3.7.14", - "urql": "^4.0.3", "@graphql-codegen/cli": "^4.0.0", "@graphql-codegen/client-preset": "^4.0.0", + "@graphql-codegen/introspection": "^4.0.0", "@graphql-codegen/schema-ast": "^4.0.0", "graphql": "^16.6.0", "graphql-codegen": "^0.4.0", "graphql-sse": "^2.1.4", "graphql-ws": "^5.13.1", "typescript": "^5.0.2", + "urql": "^4.0.3", "vite": "^4.3.9", "vitest": "^0.32.0" } diff --git a/integration/server/generated.go b/integration/server/generated.go index fe95a520e5..6928d3f2d5 100644 --- a/integration/server/generated.go +++ b/integration/server/generated.go @@ -70,8 +70,9 @@ type ComplexityRoot struct { } User struct { - Likes func(childComplexity int) int - Name func(childComplexity int) int + Likes func(childComplexity int) int + Name func(childComplexity int) int + PhoneNumber func(childComplexity int) int } Viewer struct { @@ -223,6 +224,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.User.Name(childComplexity), true + case "User.phoneNumber": + if e.complexity.User.PhoneNumber == nil { + break + } + + return e.complexity.User.PhoneNumber(childComplexity), true + case "Viewer.user": if e.complexity.Viewer.User == nil { break @@ -1224,6 +1232,47 @@ func (ec *executionContext) fieldContext_User_likes(ctx context.Context, field g return fc, nil } +func (ec *executionContext) _User_phoneNumber(ctx context.Context, field graphql.CollectedField, obj *remote_api.User) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_User_phoneNumber(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.PhoneNumber, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(string) + fc.Result = res + return ec.marshalOString2string(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_User_phoneNumber(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "User", + Field: field, + IsMethod: false, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type String does not have child fields") + }, + } + return fc, nil +} + func (ec *executionContext) _Viewer_user(ctx context.Context, field graphql.CollectedField, obj *models.Viewer) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Viewer_user(ctx, field) if err != nil { @@ -1264,6 +1313,8 @@ func (ec *executionContext) fieldContext_Viewer_user(ctx context.Context, field return ec.fieldContext_User_name(ctx, field) case "likes": return ec.fieldContext_User_likes(ctx, field) + case "phoneNumber": + return ec.fieldContext_User_phoneNumber(ctx, field) } return nil, fmt.Errorf("no field named %q was found under type User", field.Name) }, @@ -3587,6 +3638,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj } out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + case "phoneNumber": + out.Values[i] = ec._User_phoneNumber(ctx, field, obj) default: panic("unknown field " + strconv.Quote(field.Name)) } diff --git a/integration/server/remote_api/user.go b/integration/server/remote_api/user.go index e6262a29ba..feefded34a 100644 --- a/integration/server/remote_api/user.go +++ b/integration/server/remote_api/user.go @@ -1,6 +1,7 @@ package remote_api type User struct { - Name string - Likes []string + Name string + Likes []string + PhoneNumber string } diff --git a/integration/server/resolver.go b/integration/server/resolver.go index 6d82545f21..5a0d339831 100644 --- a/integration/server/resolver.go +++ b/integration/server/resolver.go @@ -87,7 +87,7 @@ func (r *queryResolver) Date(ctx context.Context, filter models.DateFilter) (boo func (r *queryResolver) Viewer(ctx context.Context) (*models.Viewer, error) { return &models.Viewer{ - User: &remote_api.User{Name: "Bob", Likes: []string{"Alice"}}, + User: &remote_api.User{Name: "Bob", Likes: []string{"Alice"}, PhoneNumber: "1234567890"}, }, nil } diff --git a/integration/server/schema/user.graphql b/integration/server/schema/user.graphql index 15586a3f00..a60bdf473c 100644 --- a/integration/server/schema/user.graphql +++ b/integration/server/schema/user.graphql @@ -1,4 +1,5 @@ type User { name: String! likes: [String!]! + phoneNumber: String @deprecated } diff --git a/integration/src/__test__/integration.spec.ts b/integration/src/__test__/integration.spec.ts index 2cd0860bb7..9bac41844f 100644 --- a/integration/src/__test__/integration.spec.ts +++ b/integration/src/__test__/integration.spec.ts @@ -8,6 +8,8 @@ import {Client as ClientSSE, ClientOptions as ClientOptionsSSE, createClient as import {CoercionDocument, ComplexityDocument, DateDocument, ErrorDocument, ErrorType, JsonEncodingDocument, PathDocument, UserFragmentFragmentDoc, ViewerDocument} from '../generated/graphql.ts'; import {cacheExchange, Client, dedupExchange, subscriptionExchange} from 'urql'; import {isFragmentReady, useFragment} from "../generated"; +import { readFileSync } from 'fs'; +import { join } from 'path'; const uri = process.env.VITE_SERVER_URL || 'http://localhost:8080/query'; @@ -225,6 +227,25 @@ describe('HTTP client', () => { }); }); +describe('Schema Introspection', () => { + + const schemaJson = readFileSync(join(__dirname, '../generated/schema-introspection.json'), 'utf-8'); + const schema = JSON.parse(schemaJson); + + it('User.phoneNumber is deprecated and deprecationReason has the default value: `No longer supported`', async () => { + + const userType = schema.__schema.types.find((type: any) => type.name === 'User'); + + expect(userType).toBeDefined(); + + const phoneNumberField = userType.fields.find((field: any) => field.name === 'phoneNumber'); + expect(phoneNumberField).toBeDefined(); + + expect(phoneNumberField.isDeprecated).toBe(true); + expect(phoneNumberField.deprecationReason).toBe('No longer supported'); + }) +}); + describe('Websocket client', () => { const client = new ApolloClient({ link: new GraphQLWsLink( diff --git a/integration/src/generated/.gitignore b/integration/src/generated/.gitignore index adcf77e747..4ccb0c3a47 100644 --- a/integration/src/generated/.gitignore +++ b/integration/src/generated/.gitignore @@ -1 +1,2 @@ schema-fetched.graphql +schema-introspection.json diff --git a/integration/src/generated/gql.ts b/integration/src/generated/gql.ts index 74067dc6ad..e1d298c3d8 100644 --- a/integration/src/generated/gql.ts +++ b/integration/src/generated/gql.ts @@ -19,7 +19,7 @@ const documents = { "query error($type: ErrorType) {\n error(type: $type)\n}": types.ErrorDocument, "query jsonEncoding {\n jsonEncoding\n}": types.JsonEncodingDocument, "query path {\n path {\n cc: child {\n error\n }\n }\n}": types.PathDocument, - "query viewer {\n viewer {\n user {\n name\n ...userFragment @defer\n }\n }\n}\n\nfragment userFragment on User {\n likes\n}": types.ViewerDocument, + "query viewer {\n viewer {\n user {\n name\n phoneNumber\n ...userFragment @defer\n }\n }\n}\n\nfragment userFragment on User {\n likes\n}": types.ViewerDocument, }; /** @@ -63,7 +63,7 @@ export function graphql(source: "query path {\n path {\n cc: child {\n /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query viewer {\n viewer {\n user {\n name\n ...userFragment @defer\n }\n }\n}\n\nfragment userFragment on User {\n likes\n}"): (typeof documents)["query viewer {\n viewer {\n user {\n name\n ...userFragment @defer\n }\n }\n}\n\nfragment userFragment on User {\n likes\n}"]; +export function graphql(source: "query viewer {\n viewer {\n user {\n name\n phoneNumber\n ...userFragment @defer\n }\n }\n}\n\nfragment userFragment on User {\n likes\n}"): (typeof documents)["query viewer {\n viewer {\n user {\n name\n phoneNumber\n ...userFragment @defer\n }\n }\n}\n\nfragment userFragment on User {\n likes\n}"]; export function graphql(source: string) { return (documents as any)[source] ?? {}; diff --git a/integration/src/generated/graphql.ts b/integration/src/generated/graphql.ts index 485adb27db..c01f4d4bb4 100644 --- a/integration/src/generated/graphql.ts +++ b/integration/src/generated/graphql.ts @@ -91,6 +91,8 @@ export type User = { __typename?: 'User'; likes: Array; name: Scalars['String']['output']; + /** @deprecated No longer supported */ + phoneNumber?: Maybe; }; export type Viewer = { @@ -139,7 +141,7 @@ export type PathQuery = { __typename?: 'Query', path?: Array<{ __typename?: 'Ele export type ViewerQueryVariables = Exact<{ [key: string]: never; }>; -export type ViewerQuery = { __typename?: 'Query', viewer?: { __typename?: 'Viewer', user?: ( { __typename?: 'User', name: string } & ( +export type ViewerQuery = { __typename?: 'Query', viewer?: { __typename?: 'Viewer', user?: ( { __typename?: 'User', name: string, phoneNumber?: string | null } & ( { __typename?: 'User' } & { ' $fragmentRefs'?: { 'UserFragmentFragment': Incremental } } ) ) | null } | null }; @@ -153,4 +155,4 @@ export const DateDocument = {"kind":"Document","definitions":[{"kind":"Operation export const ErrorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"error"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"type"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ErrorType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"type"},"value":{"kind":"Variable","name":{"kind":"Name","value":"type"}}}]}]}}]} as unknown as DocumentNode; export const JsonEncodingDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"jsonEncoding"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"jsonEncoding"}}]}}]} as unknown as DocumentNode; export const PathDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"path"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"cc"},"name":{"kind":"Name","value":"child"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]}}]} as unknown as DocumentNode; -export const ViewerDocument = {"__meta__":{"deferredFields":{"userFragment":["likes"]}},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"userFragment"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"defer"}}]}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"userFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"likes"}}]}}]} as unknown as DocumentNode; \ No newline at end of file +export const ViewerDocument = {"__meta__":{"deferredFields":{"userFragment":["likes"]}},"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"phoneNumber"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"userFragment"},"directives":[{"kind":"Directive","name":{"kind":"Name","value":"defer"}}]}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"userFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"likes"}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/integration/src/generated/schema-expected.graphql b/integration/src/generated/schema-expected.graphql index a2b46b4678..021ca6596b 100644 --- a/integration/src/generated/schema-expected.graphql +++ b/integration/src/generated/schema-expected.graphql @@ -58,6 +58,7 @@ type RemoteModelWithOmitempty { type User { likes: [String!]! name: String! + phoneNumber: String @deprecated } type Viewer { diff --git a/integration/src/queries/viewer.graphql b/integration/src/queries/viewer.graphql index 2fff32ac89..1be437c439 100644 --- a/integration/src/queries/viewer.graphql +++ b/integration/src/queries/viewer.graphql @@ -2,6 +2,7 @@ query viewer { viewer { user { name + phoneNumber ... userFragment @defer } } @@ -9,4 +10,4 @@ query viewer { fragment userFragment on User { likes -} +} \ No newline at end of file