diff --git a/packages/graphiql-react/src/schema.tsx b/packages/graphiql-react/src/schema.tsx index 6284fc95ddc..0aff0014d46 100644 --- a/packages/graphiql-react/src/schema.tsx +++ b/packages/graphiql-react/src/schema.tsx @@ -390,6 +390,7 @@ function useIntrospectionQuery({ let query = getIntrospectionQuery({ inputValueDeprecation, schemaDescription, + oneOf: true, }); if (introspectionQueryName) { query = query.replace('query IntrospectionQuery', `query ${queryName}`); diff --git a/packages/graphiql/cypress/e2e/docs.cy.ts b/packages/graphiql/cypress/e2e/docs.cy.ts index 9bf53197980..e703d29b05b 100644 --- a/packages/graphiql/cypress/e2e/docs.cy.ts +++ b/packages/graphiql/cypress/e2e/docs.cy.ts @@ -22,7 +22,7 @@ describe('GraphiQL DocExplorer - search', () => { beforeEach(() => { cy.get('.graphiql-sidebar button').eq(0).click(); cy.dataCy('doc-explorer-input').type('test'); - cy.dataCy('doc-explorer-option').should('have.length', 7); + cy.dataCy('doc-explorer-option').should('have.length', 8); }); it('Searches docs for values', () => { diff --git a/packages/graphiql/test/schema.js b/packages/graphiql/test/schema.js index d6c048e5514..4b84f92b47e 100644 --- a/packages/graphiql/test/schema.js +++ b/packages/graphiql/test/schema.js @@ -50,40 +50,54 @@ const TestEnum = new GraphQLEnumType({ const TestInputObject = new GraphQLInputObjectType({ name: 'TestInput', description: 'Test all sorts of inputs in this input object type.', - fields: () => ({ - string: { - type: GraphQLString, - description: 'Repeats back this string', - }, - int: { type: GraphQLInt }, - float: { type: GraphQLFloat }, - boolean: { type: GraphQLBoolean }, - id: { type: GraphQLID }, - enum: { type: TestEnum }, - object: { type: TestInputObject }, - defaultValueString: { - type: GraphQLString, - defaultValue: 'test default value', - }, - defaultValueBoolean: { - type: GraphQLBoolean, - defaultValue: false, - }, - defaultValueInt: { - type: GraphQLInt, - defaultValue: 5, - }, - // List - listString: { type: new GraphQLList(GraphQLString) }, - listInt: { type: new GraphQLList(GraphQLInt) }, - listFloat: { type: new GraphQLList(GraphQLFloat) }, - listBoolean: { type: new GraphQLList(GraphQLBoolean) }, - listID: { type: new GraphQLList(GraphQLID) }, - listEnum: { type: new GraphQLList(TestEnum) }, - listObject: { type: new GraphQLList(TestInputObject) }, - }), + fields: () => inputFields, +}); + +const TestOneOfInputObject = new GraphQLInputObjectType({ + name: 'TestOneOfInput', + description: 'Test @oneOf input types with this input object type.', + isOneOf: true, + fields: () => + // remove defaultValue which is not compatible with @oneOf + Object.entries(inputFields).reduce((a, [k, { defaultValue, ...value }]) => { + a[k] = value; + return a; + }, {}), }); +const inputFields = { + string: { + type: GraphQLString, + description: 'Repeats back this string', + }, + int: { type: GraphQLInt }, + float: { type: GraphQLFloat }, + boolean: { type: GraphQLBoolean }, + id: { type: GraphQLID }, + enum: { type: TestEnum }, + object: { type: TestInputObject }, + defaultValueString: { + type: GraphQLString, + defaultValue: 'test default value', + }, + defaultValueBoolean: { + type: GraphQLBoolean, + defaultValue: false, + }, + defaultValueInt: { + type: GraphQLInt, + defaultValue: 5, + }, + // List + listString: { type: new GraphQLList(GraphQLString) }, + listInt: { type: new GraphQLList(GraphQLInt) }, + listFloat: { type: new GraphQLList(GraphQLFloat) }, + listBoolean: { type: new GraphQLList(GraphQLBoolean) }, + listID: { type: new GraphQLList(GraphQLID) }, + listEnum: { type: new GraphQLList(TestEnum) }, + listObject: { type: new GraphQLList(TestInputObject) }, +}; + const TestInterface = new GraphQLInterfaceType({ name: 'TestInterface', description: 'Test interface.', @@ -250,6 +264,7 @@ const TestType = new GraphQLObjectType({ description: '`test` field from `Test` type.', resolve: () => ({}), }, + deferrable: { type: DeferrableObject, resolve: () => ({}), @@ -333,6 +348,7 @@ const TestType = new GraphQLObjectType({ id: { type: GraphQLID }, enum: { type: TestEnum }, object: { type: TestInputObject }, + oneOfObject: { type: TestOneOfInputObject }, defaultValue: { type: GraphQLString, defaultValue: 'test default value', diff --git a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts index ffbe0dc6f36..1704e4e36e1 100644 --- a/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts +++ b/packages/graphql-language-service-server/src/__tests__/MessageProcessor.spec.ts @@ -436,7 +436,7 @@ describe('MessageProcessor with config', () => { character: 0, }, end: { - line: 102 + offset, + line: 103 + offset, character: 1, }, }); @@ -450,11 +450,11 @@ describe('MessageProcessor with config', () => { // this might break, please adjust if you see a failure here expect(serializeRange(schemaDefs[0].range)).toEqual({ start: { - line: 104 + offset, + line: 105 + offset, character: 0, }, end: { - line: 112 + offset, + line: 113 + offset, character: 1, }, }); diff --git a/packages/graphql-language-service/src/interface/__tests__/__schema__/StarWarsSchema.graphql b/packages/graphql-language-service/src/interface/__tests__/__schema__/StarWarsSchema.graphql index c16bdf63983..42ac95d3774 100644 --- a/packages/graphql-language-service/src/interface/__tests__/__schema__/StarWarsSchema.graphql +++ b/packages/graphql-language-service/src/interface/__tests__/__schema__/StarWarsSchema.graphql @@ -46,6 +46,12 @@ input InputType { obj: InputType } +input OneOfInputType @oneOf { + key: String + value: Int + obj: InputType +} + interface TestInterface { """ example @@ -69,6 +75,7 @@ type Query { inputTypeTest(args: InputType = { key: "key" }): TestType deprecatedField: TestType @deprecated(reason: "Use test instead.") union: TestUnion + oneOfInputTypeTest(oneOf: OneOfInputType): String } union TestUnion = Droid | TestType diff --git a/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts b/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts index e784200f9b4..5d2507a0ef0 100644 --- a/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts +++ b/packages/graphql-language-service/src/interface/__tests__/getAutocompleteSuggestions-test.ts @@ -49,6 +49,10 @@ const expectedResults = { label: 'inputTypeTest', detail: 'TestType', }, + oneOfInputTypeTest: { + detail: 'String', + label: 'oneOfInputTypeTest', + }, appearsIn: { label: 'appearsIn', detail: '[Episode]', @@ -163,7 +167,7 @@ describe('getAutocompleteSuggestions', () => { }, { - sortText: '7__schema', + sortText: '8__schema', label: '__schema', detail: '__Schema!', }, @@ -213,6 +217,7 @@ describe('getAutocompleteSuggestions', () => { expectedResults.hero, expectedResults.human, expectedResults.inputTypeTest, + expectedResults.oneOfInputTypeTest, expectedResults.union, ]); @@ -236,6 +241,7 @@ describe('getAutocompleteSuggestions', () => { expectedResults.hero, expectedResults.human, expectedResults.inputTypeTest, + expectedResults.oneOfInputTypeTest, expectedResults.union, ]); }); @@ -250,6 +256,7 @@ describe('getAutocompleteSuggestions', () => { expectedResults.hero, expectedResults.human, expectedResults.inputTypeTest, + expectedResults.oneOfInputTypeTest, expectedResults.union, ]); }); @@ -322,6 +329,13 @@ describe('getAutocompleteSuggestions', () => { insertText: 'inputTypeTest {\n $1\n}', labelDetails: { detail: ' TestType' }, }, + { + ...expectedResults.oneOfInputTypeTest, + command: suggestionCommand, + insertTextFormat: 2, + insertText: 'oneOfInputTypeTest\n', + labelDetails: { detail: ' String' }, + }, { label: 'union', insertTextFormat: 2, @@ -419,7 +433,9 @@ describe('getAutocompleteSuggestions', () => { { label: 'Boolean', documentation: GraphQLBoolean.description }, { label: 'Episode' }, { label: 'InputType' }, + { label: 'Int', documentation: GraphQLInt.description }, + { label: 'OneOfInputType' }, { label: 'String', documentation: GraphQLString.description }, ]); }); @@ -433,7 +449,9 @@ describe('getAutocompleteSuggestions', () => { ...metaArgs, { label: 'InputType' }, + { label: 'Int', documentation: GraphQLInt.description }, + { label: 'OneOfInputType' }, { label: 'String', documentation: GraphQLString.description }, ]); }); @@ -654,7 +672,7 @@ describe('getAutocompleteSuggestions', () => { ]; it('provides correct testInput type field suggestions', () => { expect( - testSuggestions('{ inputTypeTest(args: {', new Position(0, 23)), + testSuggestions('{ inputTypeTest(args: { ', new Position(0, 24)), ).toEqual(inputArgs); }); @@ -664,6 +682,37 @@ describe('getAutocompleteSuggestions', () => { ).toEqual(inputArgs); }); + it('provides correct oneOf input type field suggestions', () => { + const args = [ + { ...inputArgs[0], detail: 'String' }, + inputArgs[1], + inputArgs[2], + ]; + expect( + testSuggestions('{ oneOfInputTypeTest(oneOf: {', new Position(0, 29)), + ).toEqual(args); + }); + + it('provides no more field suggestions once a oneOf field is chosen', () => { + expect( + testSuggestions( + '{ oneOfInputTypeTest(oneOf: { value: 2 ', + new Position(0, 40), + ), + ).toEqual([]); + }); + + // TODO: decide if we want this. Discussing with @benjie, we might want to actually give the user flexibility here, + // instead of being strict + it('provides no more field suggestions once a oneOf field is chose and a user begins typing another field', () => { + expect( + testSuggestions( + '{ oneOfInputTypeTest(oneOf: { value: 2 d', + new Position(0, 40), + ), + ).toEqual([]); + }); + it('provides correct field name suggestion inside inline fragment', () => { expect( testSuggestions( @@ -813,6 +862,7 @@ describe('getAutocompleteSuggestions', () => { it('provides input objects to be extended', () => { expect(testSuggestions('extend input ', new Position(0, 13))).toEqual([ { label: 'InputType' }, + { label: 'OneOfInputType' }, ]); }); @@ -847,8 +897,10 @@ describe('getAutocompleteSuggestions', () => { ).toEqual([ { label: 'Boolean' }, { label: 'Episode' }, + { label: 'InputType' }, { label: 'Int' }, + { label: 'OneOfInputType' }, { label: 'String' }, ])); diff --git a/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts b/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts index dfc055cba8b..eb05178ca0c 100644 --- a/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts +++ b/packages/graphql-language-service/src/interface/getAutocompleteSuggestions.ts @@ -312,11 +312,23 @@ export function getAutocompleteSuggestions( (kind === RuleKinds.OBJECT_FIELD && step === 0)) && typeInfo.objectFieldDefs ) { - const objectFields = objectValues(typeInfo.objectFieldDefs); + const { inputType, objectFieldDefs } = typeInfo; + const { string: tokenString } = token; + if ( + inputType && + 'isOneOf' in inputType && + inputType?.isOneOf === true && + (prevState?.prevState?.kind !== 'Argument' || tokenString !== '{') + ) { + // return empty array early if a oneOf field has already been provided + return []; + } + const objectFields = objectValues(objectFieldDefs); const completionKind = kind === RuleKinds.OBJECT_VALUE ? CompletionItemKind.Value : CompletionItemKind.Field; + return hintList( token, objectFields.map(field => ({ diff --git a/packages/graphql-language-service/src/parser/getTypeInfo.ts b/packages/graphql-language-service/src/parser/getTypeInfo.ts index f0f8d1ae8d7..656c3015964 100644 --- a/packages/graphql-language-service/src/parser/getTypeInfo.ts +++ b/packages/graphql-language-service/src/parser/getTypeInfo.ts @@ -213,8 +213,11 @@ export function getTypeInfo( } } } - inputType = argDef?.type; + if (argDef?.type) { + inputType = argDef.type; + } break; + case RuleKinds.VARIABLE_DEFINITION: case RuleKinds.VARIABLE: type = inputType; @@ -241,12 +244,15 @@ export function getTypeInfo( objectType instanceof GraphQLInputObjectType ? objectType.getFields() : null; + inputType = objectType; break; // TODO: needs tests case RuleKinds.OBJECT_FIELD: const objectField = state.name && objectFieldDefs ? objectFieldDefs[state.name] : null; - inputType = objectField?.type; + if (objectField?.type) { + inputType = objectField?.type; + } // @ts-expect-error fieldDef = objectField as GraphQLField; type = fieldDef ? fieldDef.type : null; diff --git a/yarn.lock b/yarn.lock index e2c7653637e..5096aba04a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10538,7 +10538,12 @@ graphql-ws@5.14.0, graphql-ws@^5.5.5: resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.14.0.tgz#766f249f3974fc2c48fae0d1fb20c2c4c79cd591" integrity sha512-itrUTQZP/TgswR4GSSYuwWUzrE/w5GhbwM2GX3ic2U7aw33jgEsayfIlvaj7/GcIvZgNMzsPTrE5hqPuFUiE5g== -"graphql@^16.8.1 || ^17.0.0-alpha.2", graphql@^16.9.0: +"graphql@^16.8.1 || ^17.0.0-alpha.2": + version "17.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-17.0.0-alpha.7.tgz#707e7457d7ed5316a8d7940f78809a2eb5854383" + integrity sha512-kdteHez9s0lfNAGntSwnDBpxSl09sBWEFxFRPS/Z8K1nCD4FZ2wVGwXuj5dvrTKcqOA+O8ujAJ3CiY/jXhs14g== + +graphql@^16.9.0: version "16.9.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.9.0.tgz#1c310e63f16a49ce1fbb230bd0a000e99f6f115f" integrity sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==