Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

language support for @oneOf #3769

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/graphiql-react/src/schema.tsx
Original file line number Diff line number Diff line change
@@ -390,6 +390,7 @@ function useIntrospectionQuery({
let query = getIntrospectionQuery({
inputValueDeprecation,
schemaDescription,
oneOf: true,
});
if (introspectionQueryName) {
query = query.replace('query IntrospectionQuery', `query ${queryName}`);
2 changes: 1 addition & 1 deletion packages/graphiql/cypress/e2e/docs.cy.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
80 changes: 48 additions & 32 deletions packages/graphiql/test/schema.js
Original file line number Diff line number Diff line change
@@ -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',
Original file line number Diff line number Diff line change
@@ -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,
},
});
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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' },
]));

Original file line number Diff line number Diff line change
@@ -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 => ({
10 changes: 8 additions & 2 deletions packages/graphql-language-service/src/parser/getTypeInfo.ts
Original file line number Diff line number Diff line change
@@ -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<null, null>;
type = fieldDef ? fieldDef.type : null;
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
@@ -10538,7 +10538,12 @@ [email protected], 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==