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

[WIP] Extend the extend functionality #1322

Closed
wants to merge 12 commits into from
Closed

[WIP] Extend the extend functionality #1322

wants to merge 12 commits into from

Conversation

grenierdev
Copy link

This is a WIP implementation for extending enum, input and union. See discussion in issue #1095

@facebook-github-bot
Copy link

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!

isEnumType,
GraphQLEnumType,
isInputObjectType,
GraphQLInputObjectType,
Copy link
Member

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.

name: type.name,
description: type.description,
fields: () => extendInputFieldMap(type),
astNode: type.astNode,
Copy link
Member

@IvanGoncharov IvanGoncharov Apr 22, 2018

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.

return new GraphQLEnumType({
name: type.name,
values: extendValueMap(type),
astNode: type.astNode,
Copy link
Member

Choose a reason for hiding this comment

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

Also missing extensionASTNodes.

return new GraphQLUnionType({
name: type.name,
description: type.description,
types: type.getTypes().map(getExtendedType),
types: types,
astNode: type.astNode,
resolveType: type.resolveType,
Copy link
Member

Choose a reason for hiding this comment

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

Also missing extensionASTNodes.

@@ -280,6 +379,18 @@ export function extendSchema(
});
}

function extendArgsMap(
args: GraphQLArgument[],
Copy link
Member

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.

function extendArgsMap(
args: GraphQLArgument[],
): GraphQLArgument[] {
return args.map(arg => ({
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 better to use keyValMap here so the result of this function can be assigned to GraphQLFieldConfig::args without additional manipulations.

const oldValueMap = Object.create(null);
const newValueMap = Object.create(null);
type.getValues().forEach(value => {
newValueMap[value.name] = oldValueMap[value.name] = {
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 code here should match extendFieldMap:

const oldFieldMap = type.getFields();
Object.keys(oldFieldMap).forEach(fieldName => {

So you either change extendFieldMap to do the same code or remove assigment chain here.

[field],
);
}
newFieldMap[fieldName] = astBuilder.buildField(field);
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't use buildField here since it return type/interface field. You should extract new function from _makeInputValues.

newValueMap[value.name] = oldValueMap[value.name] = {
name: value.name,
description: value.description,
value: value.value,
Copy link
Member

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:

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.

@IvanGoncharov
Copy link
Member

@mgrenier Look nice. I left some review comments also, please fix Travis errors.
You can reproduce them by running npm t.

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.

@facebook-github-bot
Copy link

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Facebook open source project. Thanks!

@grenierdev
Copy link
Author

@IvanGoncharov I still have to wrap my head around the directive argument case. Most tests cases I tried gave me errors.

extendSchema.js

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,
  }));
}

extendSchema-test.js

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 extendArgs, function to extend args type, result in a mixup reference : Error: Schema must contain unique named types but contains multiple types named "TestInput".

I need some help on this one.

@@ -291,6 +302,7 @@ export function extendSchema(
Object.keys(oldFieldMap).forEach(fieldName => {
const field = oldFieldMap[fieldName];
newFieldMap[fieldName] = {
// name: fieldName,
Copy link
Member

@IvanGoncharov IvanGoncharov Apr 26, 2018

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 ❌

description: arg.description,
astNode: arg.astNode,
}));
function extendArgs(args: GraphQLArgument[]): GraphQLFieldConfigArgumentMap {
Copy link
Member

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

arg => arg.name,
arg => ({
name: arg.name,
type: extendFieldType(arg.type),
Copy link
Member

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?

Copy link
Author

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 ?

Copy link
Member

Choose a reason for hiding this comment

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

@mgrenier I think it should be extendType and extendNamedType but it will make this review hard to read so I created #1327.

args,
arg => arg.name,
arg => ({
name: arg.name,
Copy link
Member

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:

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

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

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)

@IvanGoncharov
Copy link
Member

I still have to wrap my head around the directive argument case. Most tests cases I tried gave me errors.

@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 extensionASTNodes, extending schema multiple times, all errors (e.g. adding duplicate enum value or input field), values of enum values, etc.
When possible it's better to extend existing tests.

@IvanGoncharov
Copy link
Member

@mgrenier Also please rebase your PR on top of master, since we updated Flow & Prettier.

newValueMap[valueName] = {
name: value.name,
description: value.description,
value: value.hasOwnProperty('value') ? value.value : valueName,
Copy link
Member

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.

@IvanGoncharov
Copy link
Member

@mgrenier I review you PR one more time and found that you also need to extend this function:

function checkExtensionNode(type, node) {

@grenierdev
Copy link
Author

All review and rebase have been done

@jgcmarins
Copy link

@mgrenier it still has conflicts with src/utilities/extendSchema.js

@IvanGoncharov
Copy link
Member

it still has conflicts with src/utilities/extendSchema.js

@jgcmarins @mgrenier It's minor conflict because of #1323


extend enum SomeEnum {
DEPRECATED @deprecated(reason: "do not use")
}
Copy link
Member

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

@IvanGoncharov IvanGoncharov May 10, 2018

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
}

Copy link
Member

@IvanGoncharov IvanGoncharov May 10, 2018

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

@IvanGoncharov
Copy link
Member

invariant(existingDirectives, 'schema must have default directives');

return existingDirectives.concat(
directiveDefinitions.map(node => astBuilder.buildDirective(node)),
);
}

function extendDirectiveType(type: GraphQLDirective): GraphQLDirective {
Copy link
Member

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

Copy link
Contributor

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

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?

Copy link
Contributor

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

@IvanGoncharov IvanGoncharov May 10, 2018

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

@IvanGoncharov
Copy link
Member

@mgrenier Do you need any help with this PR?
We are planning 14.0.0 and it would be great to include this PR.
I think this PR is ready it just need to be rebased and address review comments in tests.

@IvanGoncharov IvanGoncharov mentioned this pull request Jun 4, 2018
@grenierdev
Copy link
Author

@IvanGoncharov sure do. I dont have much time these days. Sorry for the delays.

If someone can dig in, feel free.

mjmahone added a commit that referenced this pull request Jun 7, 2018
* 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
@mjmahone
Copy link
Contributor

mjmahone commented Jun 7, 2018

This was merged in #1373

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