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

Allow interface type extensions #1222

Merged
merged 5 commits into from
Feb 8, 2018

Conversation

lillyfwang
Copy link
Contributor

@lillyfwang lillyfwang commented Feb 2, 2018

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
Copy link
Member

IvanGoncharov commented Feb 5, 2018

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 👍

[def],
);
}
// Extend all of the object types that implement this interface
Copy link
Member

@IvanGoncharov IvanGoncharov Feb 5, 2018

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
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
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
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
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
}

Copy link
Contributor

@leebyron leebyron left a comment

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

[def],
);
}
// Extend all of the object types that implement this interface
Copy link
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
}

@@ -232,6 +272,17 @@ export function extendSchema(
astNode: schema.astNode,
});

function appendExtensionToTypeExtensions(
Copy link
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
Contributor Author

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

case Kind.INTERFACE_TYPE_EXTENSION:
const extendedInterfaceTypeName = def.name.value;
Copy link
Member

@IvanGoncharov IvanGoncharov Feb 6, 2018

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
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
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
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', () => {

]);
});

it('accepts Objects implementing the extended interface by extending Objects with fields', () => {
Copy link
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.

]);
});

it('accepts an Object implementing the extended interface with existing field', () => {
Copy link
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.

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
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
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
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).

Copy link
Member

@IvanGoncharov IvanGoncharov Feb 7, 2018

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
Copy link
Member

IvanGoncharov commented Feb 8, 2018

@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
Copy link
Contributor

leebyron commented Feb 8, 2018

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