-
Notifications
You must be signed in to change notification settings - Fork 2k
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
[WIP] Extend the extend functionality #1322
Conversation
Thank you for your pull request and welcome to our community. We require contributors to sign our Contributor License Agreement, and we don't seem to have you on file. In order for us to review and merge your code, please sign up at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need the corporate CLA signed. If you have received this in error or have any questions, please contact us at cla@fb.com. Thanks! |
src/utilities/extendSchema.js
Outdated
isEnumType, | ||
GraphQLEnumType, | ||
isInputObjectType, | ||
GraphQLInputObjectType, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please put function imports together with other function imports.
src/utilities/extendSchema.js
Outdated
name: type.name, | ||
description: type.description, | ||
fields: () => extendInputFieldMap(type), | ||
astNode: type.astNode, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to add extensionASTNodes
here. But first, you need to add it to GraphQLInputObjectType
class.
src/utilities/extendSchema.js
Outdated
return new GraphQLEnumType({ | ||
name: type.name, | ||
values: extendValueMap(type), | ||
astNode: type.astNode, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also missing extensionASTNodes
.
src/utilities/extendSchema.js
Outdated
return new GraphQLUnionType({ | ||
name: type.name, | ||
description: type.description, | ||
types: type.getTypes().map(getExtendedType), | ||
types: types, | ||
astNode: type.astNode, | ||
resolveType: type.resolveType, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also missing extensionASTNodes
.
src/utilities/extendSchema.js
Outdated
@@ -280,6 +379,18 @@ export function extendSchema( | |||
}); | |||
} | |||
|
|||
function extendArgsMap( | |||
args: GraphQLArgument[], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It accepts and returns an array so it probably shouldn't have Map
in its name.
src/utilities/extendSchema.js
Outdated
function extendArgsMap( | ||
args: GraphQLArgument[], | ||
): GraphQLArgument[] { | ||
return args.map(arg => ({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's better to use keyValMap
here so the result of this function can be assigned to GraphQLFieldConfig::args
without additional manipulations.
src/utilities/extendSchema.js
Outdated
const oldValueMap = Object.create(null); | ||
const newValueMap = Object.create(null); | ||
type.getValues().forEach(value => { | ||
newValueMap[value.name] = oldValueMap[value.name] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think code here should match extendFieldMap
:
graphql-js/src/utilities/extendSchema.js
Lines 335 to 336 in 3cb4500
const oldFieldMap = type.getFields(); | |
Object.keys(oldFieldMap).forEach(fieldName => { |
So you either change
extendFieldMap
to do the same code or remove assigment chain here.
src/utilities/extendSchema.js
Outdated
[field], | ||
); | ||
} | ||
newFieldMap[fieldName] = astBuilder.buildField(field); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't use buildField
here since it return type/interface field. You should extract new function from _makeInputValues
.
src/utilities/extendSchema.js
Outdated
newValueMap[value.name] = oldValueMap[value.name] = { | ||
name: value.name, | ||
description: value.description, | ||
value: value.value, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't do that, because of this check:
graphql-js/src/type/definition.js
Line 1164 in 3cb4500
value: value.hasOwnProperty('value') ? value.value : valueName, |
In the current version, it will convert all enum values without explicit
value
to undefined
.This is the very important case so please add a separate test for it.
@mgrenier Look nice. I left some review comments also, please fix Travis errors. One important feature thing missing in this PR: Since you extending types that can be used in directive arguments that mean you need to recreate directives with updated arguments. |
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks! |
@IvanGoncharov I still have to wrap my head around the directive argument case. Most tests cases I tried gave me errors.
function getMergedDirectives(): Array<GraphQLDirective> {
const existingDirectives = schema.getDirectives();
invariant(existingDirectives, 'schema must have default directives');
return existingDirectives.concat(
directiveDefinitions.map(node => astBuilder.buildDirective(node)),
).map(directive => new GraphQLDirective({
name: directive.name,
description: directive.description,
locations: directive.locations,
args: extendArgs(directive.args),
astNode: directive.astNode,
}));
}
it('extend input', () => {
const extendedSchema = extendTestSchema(`
extend type Query {
newField(testArg: TestInput): String
}
input TestInput {
testInputField: String
}
directive @test(arg: TestInput) on FIELD
`); Seems like just passing the directives args trough the newly I need some help on this one. |
src/utilities/extendSchema.js
Outdated
@@ -291,6 +302,7 @@ export function extendSchema( | |||
Object.keys(oldFieldMap).forEach(fieldName => { | |||
const field = oldFieldMap[fieldName]; | |||
newFieldMap[fieldName] = { | |||
// name: fieldName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need it ❌
src/utilities/extendSchema.js
Outdated
description: arg.description, | ||
astNode: arg.astNode, | ||
})); | ||
function extendArgs(args: GraphQLArgument[]): GraphQLFieldConfigArgumentMap { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use Array similar to other places in this file
src/utilities/extendSchema.js
Outdated
arg => arg.name, | ||
arg => ({ | ||
name: arg.name, | ||
type: extendFieldType(arg.type), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a critical issue but you could think of a better name for this function since now it also used for arguments. How about extendType
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extendType
is already used in this scope. Should I move the logic of extendFieldType
into this function or used something like extendValueType
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/utilities/extendSchema.js
Outdated
args, | ||
arg => arg.name, | ||
arg => ({ | ||
name: arg.name, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need name
, see:
graphql-js/src/type/definition.js
Lines 842 to 847 in ae5b163
export type GraphQLArgumentConfig = { | |
type: GraphQLInputType, | |
defaultValue?: mixed, | |
description?: ?string, | |
astNode?: ?InputValueDefinitionNode, | |
}; |
const newEnum = extendedSchema.getType('SomeEnum'); | ||
const testDirective = extendedSchema.getDirective('test'); | ||
|
||
expect(oldEnum._values.length).to.equal(2); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: expect(oldEnum._values).to.have.lengthOf(2)
expect(oldEnum._values.length).to.equal(2); | ||
expect(newEnum._values.length).to.equal(3); | ||
expect(newEnum._values[2].name).to.equal('NEW_ENUM'); | ||
expect(newEnum).to.equal(testDirective.args[0].type); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: expect(testDirective).to.have.nested.property('args[0].type', newEnum)
@mgrenier You code is correct, except you shouldn't recreate new directives, only old ones: function getMergedDirectives(): Array<GraphQLDirective> {
const existingDirectives = schema.getDirectives() );
invariant(existingDirectives, 'schema must have default directives');
return existingDirectives.concat(
existingDirectives.map(
directive =>
new GraphQLDirective({
name: directive.name,
description: directive.description,
locations: directive.locations,
args: extendArgs(directive.args),
astNode: directive.astNode,
}),
),
directiveDefinitions.map(node => astBuilder.buildDirective(node)),
);
} If you move out directive recreation into separate function it will make this code more readable. Also don't forget that all code should be tested e.g.: values of |
@mgrenier Also please rebase your PR on top of master, since we updated Flow & Prettier. |
src/utilities/extendSchema.js
Outdated
newValueMap[valueName] = { | ||
name: value.name, | ||
description: value.description, | ||
value: value.hasOwnProperty('value') ? value.value : valueName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, I got confused with something else 😞
You don't need this code, it should be value: value.value
.
@mgrenier I review you PR one more time and found that you also need to extend this function: graphql-js/src/utilities/extendSchema.js Line 409 in ae5b163
|
All review and rebase have been done |
@mgrenier it still has conflicts with |
@jgcmarins @mgrenier It's minor conflict because of #1323 |
|
||
extend enum SomeEnum { | ||
DEPRECATED @deprecated(reason: "do not use") | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test name is "extends objects with deprecated fields"
So I think it's better to create a separate one for enum values.
someInput: String | ||
} | ||
|
||
union UnusedUnion = Unused |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think extends schema by adding new unused types
would be a better name for this test.
union UnusedUnion = Unused
Unused
became used 😄 It's better to create separate DummyUnionMember
type.
@@ -507,6 +595,34 @@ describe('extendSchema', () => { | |||
interface NewInterface { | |||
buzz: String | |||
} | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test description: extends objects multiple times
=> extends types multiple times
@mgrenier I notice decreased coverage: |
invariant(existingDirectives, 'schema must have default directives'); | ||
|
||
return existingDirectives.concat( | ||
directiveDefinitions.map(node => astBuilder.buildDirective(node)), | ||
); | ||
} | ||
|
||
function extendDirectiveType(type: GraphQLDirective): GraphQLDirective { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function name is confusing it should be either extendDirective
or extendDirectiveTypes
(I like it more).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going with extendDirective
@@ -257,14 +267,24 @@ export function extendSchema( | |||
// this scope and have access to the schema, cache, and newly defined types. | |||
|
|||
function getMergedDirectives(): Array<GraphQLDirective> { | |||
const existingDirectives = schema.getDirectives(); | |||
const existingDirectives = schema.getDirectives().map(extendDirectiveType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we shouldn't recreate standard directives like @skip
, @include
and @deprecate
.
@leebyron What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really elegant, so we can start with this. It would be a breaking change to disallow extending @skip
/etc. but I like being able to extend them just like any other directive.
const valueName = value.name.value; | ||
if (oldValueMap[valueName]) { | ||
throw new GraphQLError( | ||
`Enum "${type.name}.${valueName}" already exists in the ` + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enum
=> Enum value
or something similar
@mgrenier Do you need any help with this PR? |
@IvanGoncharov sure do. I dont have much time these days. Sorry for the delays. If someone can dig in, feel free. |
* Extend the extend functionality * Fix flowtype * Extract logic from _makeInputValues to its own function * Fix lint and minor refactors * Fix type error due to copy & paste * Fix array syntax * Fix test more verbose * Removed unnecessary check * Extend directive * Moved new tests into existing tests * Fix lint errors * Added missing description * fix enum value error test
This was merged in #1373 |
This is a WIP implementation for extending enum, input and union. See discussion in issue #1095