Skip to content

Allow interface type extensions - #1222

Merged
leebyron merged 5 commits into
graphql:masterfrom
lillyfwang:extend-interface-types
Feb 8, 2018
Merged

Allow interface type extensions#1222
leebyron merged 5 commits into
graphql:masterfrom
lillyfwang:extend-interface-types

Conversation

@lillyfwang

@lillyfwang lillyfwang commented Feb 2, 2018

Copy link
Copy Markdown
Contributor

The GraphQL SDL was recently updated to allow interface type extensions (along with other GraphQL type extensions), but extendSchema() hasn't been updated to actually support these type extensions (as noted in #1095). This PR adds support for extending interface types by extending every GraphQLObject type that implement the interface with the same new fields and directives. It also extends the interface type itself by updating the extensionASTNodes property for the GraphQLInterface Type.

Edit: Just saw #1199 - are there major conflicts with those changes/future work? It didn't seem like it at first glance.

@IvanGoncharov

IvanGoncharov commented Feb 5, 2018

Copy link
Copy Markdown
Member

Edit: Just saw #1199 - are there major conflicts with those changes/future work? It didn't seem like it at first glance.

@lillyfwang Don't worry about it. My PR has a more long-term goal to allow simplify Schema transformation in general. Your PR is very helpful since it adds more test-cases and challenges transformSchema to handle more use cases 👍

Comment thread src/utilities/extendSchema.js Outdated
[def],
);
}
// Extend all of the object types that implement this interface

@IvanGoncharov IvanGoncharov Feb 5, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@lillyfwang You shouldn't do that, if you want to extend interface you shouldn't also extend all objects that implement this interface with missing fields:

extend interface SomeInterface {
  newInterface: NewInterface
}

extend type Foo {
  newInterface: NewObject
}

extend type Bar {
  newInterface: NewOtherObject
}

Note: similar to how you normally define a type that implements an interface you can use compatible type. That's why you can't auto-implement interfaces 😞

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@IvanGoncharov I'm not sure if I'm understanding your comment properly, but is your concern here that "NewOtherObject" could possibly not implement "NewInterface"? Is a solution here to validate the schema after extending it to make sure the new extended schema is valid?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@lillyfwang No I mean you can't do:

Extend all of the object types that implement this interface

You shouldn't add fields to GraphQLObjectType since extend interface should affect only interface not object types implementing this interface.

For example, if you use bellow schema:

interface SomeInterface {
  foo: String
}

type SomeObject {
  foo: String
}

Incorrect extension (correct in your PR):

extend interface SomeInterface {
  bar: String
}

Correct extension:

extend interface SomeInterface {
  bar: String
}

extend type SomeObject {
  bar: String
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Oh okay I understand - thanks for explaining!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree with @IvanGoncharov here - we want to preserve the use case for extending interface fields when types already have the fields defined, and we also want to ensure we can extend objects with a more specific variant of that field, example:

extend interface SomeInterface {
  newField: SomeInterface
}

extend type SomeObject { # Assuming it already implements SomeInterface
  newField: SomeObject # This is okay now, since SomeObject is more specific than SomeInterface
}

@leebyron leebyron left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is looking great, @lillyfwang - thanks for taking this on.

I think with a couple more tests and addressing the feedback this should be ready to merge

Comment thread src/utilities/extendSchema.js Outdated
[def],
);
}
// Extend all of the object types that implement this interface

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I agree with @IvanGoncharov here - we want to preserve the use case for extending interface fields when types already have the fields defined, and we also want to ensure we can extend objects with a more specific variant of that field, example:

extend interface SomeInterface {
  newField: SomeInterface
}

extend type SomeObject { # Assuming it already implements SomeInterface
  newField: SomeObject # This is okay now, since SomeObject is more specific than SomeInterface
}

astNode: schema.astNode,
});

function appendExtensionToTypeExtensions(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nice!

@lillyfwang

Copy link
Copy Markdown
Contributor Author

Just saw @leebyron's comment...updating with more tests now

break;
case Kind.SCALAR_TYPE_EXTENSION:
case Kind.INTERFACE_TYPE_EXTENSION:
const extendedInterfaceTypeName = def.name.value;

@IvanGoncharov IvanGoncharov Feb 6, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: can you please move this case before Kind.DIRECTIVE_DEFINITION

`Cannot extend interface "${extendedInterfaceTypeName}" because ` +
'it does not exist in the existing schema.',
[def],
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You need to test it similar to how it's down for object type:

it('does not allow extending an unknown type', () => {

throw new GraphQLError(
`Cannot extend non-interface type "${extendedInterfaceTypeName}".`,
[def],
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Same here:

describe('does not allow extending a non-object type', () => {

] = appendExtensionToTypeExtensions(
def,
typeExtensionsMap[extendedInterfaceTypeName],
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You need to test multiple extension of the same interface:

it('extends objects multiple times', () => {

Comment thread src/type/__tests__/validation-test.js Outdated
]);
});

it('accepts Objects implementing the extended interface by extending Objects with fields', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It makes sense to test failing cases because error messages should have correct locations (extend AST nodes) inside error objects.
But I'm not sure if it worth to test passing test here since they already covered in extendSchema.

Comment thread src/type/__tests__/validation-test.js Outdated
]);
});

it('accepts an Object implementing the extended interface with existing field', () => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think it's more appropriate to have this test as part of extendSchema test suite.

Comment thread src/type/__tests__/validation-test.js Outdated
message:
'Interface field argument AnotherInterface.newField(test:) expected but AnotherObject.newField does not provide it.',
locations: [{ line: 3, column: 20 }, { line: 7, column: 11 }],
path: undefined,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You can remove path here since it's compared with containSubset

type Bar implements SomeInterface {
name: String
some: SomeInterface
foo: Foo

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Probably bug here, should fail because both Foo and Bar are missing additional fields:

 newObject: NewObject
 newInterface: NewInterface
 newUnion: NewUnion
 newScalar: NewScalar
 newTree: [Foo]!
 newField(arg1: String, arg2: NewEnum!): String

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well printSchema should not fail here: we should be able to parse, print and extend invalid schemas. Only validateSchema(extendedSchema) should fail.

But if that's what you're testing, you should be explicit "can parse and print schema extension with missing Object fields", then prove that it's invalid by testing validateSchema fails (don't test the actual error message though, as we don't wan to force people to update this test when they update that error message).

@IvanGoncharov IvanGoncharov Feb 7, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@mjmahone You are right, I completely forget about that.
But I still think this particular test should contain valid SDL examples to not confuse readers.

But if that's what you're testing, you should be explicit "can parse and print schema extension with missing Object fields", then prove that it's invalid by testing validateSchema fails (don't test the actual error message though, as we don't wan to force people to update this test when they update that error message).

Totally make sense to add one such test for buildShema and extendSchema functions 👍

@IvanGoncharov

IvanGoncharov commented Feb 8, 2018

Copy link
Copy Markdown
Member

@lillyfwang Looks great now 👍
One issue is that you PR conflicting with master because of #1226
Can you please rebase your PR?

@leebyron
leebyron merged commit 2986c2d into graphql:master Feb 8, 2018
@leebyron

leebyron commented Feb 8, 2018

Copy link
Copy Markdown
Contributor

Excellent work, @lillyfwang!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants