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

Add a utility to detect description changes #1127

Closed
wants to merge 3 commits into from

Conversation

eapache
Copy link
Member

@eapache eapache commented Dec 7, 2017

We use this internally for pinging docs team for PR reviews.

Discussed briefly at #981 cc @leebyron.

Flow is complaining about some of this code, but Flow is just wrong; the equivalence check between newType and oldType provides the necessary safety guarantees. Not sure what the preferred solution is here.

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 looks great. I've added some review notes to help you get Flow to pass and with suggestions to simplify the code and improve the usefulness of it's results.

}
}

if (oldType && !(newType instanceof oldType.constructor)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Flow cannot see this as a cross-type refinement, plus it's a bit challenging to understand. I would instead make instanceof invariants for the oldType within each case below

Copy link
Member Author

Choose a reason for hiding this comment

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

I did it this way because spelling out the instanceofs actually becomes really messy in its own right to handle the cases where oldType is null/undefined, and where oldType is present but a different kind from newType. I'll play with it a bit to see if I can make it nicer, but any suggestions would be very welcome.

Copy link
Contributor

Choose a reason for hiding this comment

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

In this case, below line 50, you could just mirror the if statement above in an invariant while including the falsey check:

invariant(
  !oldType ||
  oldType instanceof GraphQLObjectType ||
  oldType instanceof GraphQLInterfaceType ||
  oldType instanceof GraphQLInputObjectType,
  'Expected oldType to also have fields'
);

newType instanceof GraphQLInterfaceType ||
newType instanceof GraphQLInputObjectType
) {
const oldTypeFields: ?GraphQLFieldMap<*, *> = oldType
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this differentiate between the old type not existing vs the old type existing but the field not existing?

It could be overwhelming to see a ton of "Description added on new field" for every field on a type that was newly added, that might not be helpful.

Instead perhaps "New type added with description" and "New field added with description" should be different from "Description added on field"

Copy link
Member Author

Choose a reason for hiding this comment

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

It could be overwhelming to see a ton of "Description added on new field" for every field on a type that was newly added, that might not be helpful.

The goal was to provide our docs team with a comprehensive set of all the new strings that needed reviewing; I'll type these differently when I add stronger types per your earlier comment, and then the caller can filter as desired.

if (newField.description) {
if (!oldField) {
descriptionChanges.push(
`Description added on new field ${newType.name}.${newField.name}`,
Copy link
Contributor

Choose a reason for hiding this comment

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

This wording is a little confusing. How do you add to something that is new? Add implies additive to the existing state

export function findDescriptionChanges(
oldSchema: GraphQLSchema,
newSchema: GraphQLSchema,
): Array<string> {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would suggest mirroring the return value of the breaking change detection which offers what kind of detection was encountered. It looks like there are a handful of types here to differentiate: new types, new fields on existing types, added descriptions where there were none, descriptions changed, descriptions removed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, if these are complex objects, you could contain references to the old/new schema elements themselves, that would allow you to use the output of this function to build a review tool. Currently you just have english text which requires manually looking up the described types to see what changes actually occurred, which would be limited in usefulness.

);
}
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Much of the logic above looks repetitive, with the exception of the strings referencing each kind of type. Could you factor out the common logic?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll take a look. Javascript isn't my forte so for a first pass I definitely stuck with a simpler more explicit style.

const oldField = oldTypeFields ? oldTypeFields[fieldName] : null;
const newField = newTypeFields[fieldName];

if (newField.description) {
Copy link

Choose a reason for hiding this comment

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

if (newField && newField.description) {

@eapache
Copy link
Member Author

eapache commented Jan 2, 2018

Sorry for the delay on this; finally got flow passing and I think I addressed all the code review comments. Please let me know if there's anything else.

@leebyron
Copy link
Contributor

Thanks for the update and sorry for the review delay - it appears the test coverage is still below where we usually try to keep it. Here's a link from the coveralls report: https://coveralls.io/builds/14868261/source?filename=src%2Futilities%2FfindDescriptionChanges.js

Could you add more tests to ensure all aspects of the new functionality are well tested?

We use this internally for pinging docs team for PR reviews.
Strongly type the return objects, use invariants in a way to satisfy
flow, and DRY up one common function.
@eapache
Copy link
Member Author

eapache commented Apr 2, 2018

Sorry for the delay again, I think we're both just busy :)

I've added some tests, so Coveralls now says overall coverage has increased. There's only one line (86) that's not covered at all and I'm not sure why - is newField.args expected to be truthy even when the field has no arguments? Should that check be adjusted, or removed? Otherwise (with the exception of "branch missed" warnings I don't know what to do with since it's not clear which branch was missed) everything should be covered.

oldType instanceof GraphQLObjectType ||
oldType instanceof GraphQLInterfaceType ||
oldType instanceof GraphQLInputObjectType,
'Expected oldType to also have 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 shouldn't be an error.
For example:

scalar Date
type Date {
  day: Int
  month: Int
  year: Int
}

Copy link
Member Author

Choose a reason for hiding this comment

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

This was added to satisfy Flow (#1127 (comment)).

This (and the enum case) are breaking schema changes, so an error seemed appropriate? Would you prefer they were silently ignored? If so, how do I do that in a way that Flow will recognize?

Copy link
Member

@IvanGoncharov IvanGoncharov Apr 2, 2018

Choose a reason for hiding this comment

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

So if I do breaking change in my schema I don't need to notify my documentation team about other non-breaking changes.
BTW Breaking changes is normal in many cases for example if you have your API in public beta.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, you're right. I was thinking too much about our specific case (where breaking changes already ping a substantial set of people for review and the docs changes are less relevant) but in the general case this should absolutely work.

I'm still not sure how to satisfy Flow without an invariant, but that's purely me not being much of a javascript developer.

invariant(
!oldType || oldType instanceof GraphQLEnumType,
'Expected oldType to also have values',
);
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:

scalar Month
enum Month {
  Jan
  Feb
  # ...
}

FIELD: 'FIELD',
TYPE: 'TYPE',
ARGUMENT: 'ARGUMENT',
ENUM_VALUE: 'ENUM_VALUE',
Copy link
Member

Choose a reason for hiding this comment

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

Your missing directives + you need to check arguments of directives in findDescriptionChanges

};

export const DescriptionChangeType = {
OBJECT_ADDED: 'OBJECT_ADDED',
Copy link
Member

Choose a reason for hiding this comment

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

As I understand it trigger when any new type is added not only GraphQLObjectType. So I think it should have a different name.

@@ -0,0 +1,163 @@
/* eslint-disable no-restricted-syntax */
// @flow
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 the same header license + @flow strict as other files

@@ -0,0 +1,163 @@
/* eslint-disable no-restricted-syntax */
Copy link
Member

Choose a reason for hiding this comment

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

Please remove and fix all eslint errors if any.

@IvanGoncharov
Copy link
Member

IvanGoncharov commented Apr 2, 2018

@eapache Sorry for not reviewing it earlier.

My 2¢: Different developers and teams have different needs but we can't add find*Changes function for every case. So I think we need more generic solutions for finding the difference between two schemas.

From #981:

aka anything that might need to be reviewed by a docs team

In that case changes in deprecationReason is also relevant and should be reviewed so we need to add findDeprecationReasonChanges.

Moreover, some documentation teams would be interested in being notified of new types without description.

I also see a use case for detecting any non-documentation (not in description or deprecationReason) change.

So I think instead of adding new find*Changes function for every use case we should provide single diffSchemas function. That returns something like this:

type SchemaDifference = {|
  added: {|
    queryType?: GraphQLObjectType,
    // ...
    types: Array<GraphQLTypes>,
    directives: Array<GraphQLDirectives>,
  |},
  changed: {|
    queryType?: ValueDifference<GraphQLType>,
    // ...
    types: Array<TypeDifference>,
    directives: Array<DirectivesDifference>,
  |},
  // ...
|};

type ValueDifference<T> = { old: T, new: T };

type TypeDifference = ScalarTypeDifference | ObjectTypeDifference /* ... */

type ObjectTypeDifference = {|
  name: string,
  added: {|
    description: string,
    fields: Array<GraphQLField>,
    // ...
  |},
  changed: {|
    description: ValueDifference<string>,
    fields: Array<FieldDifference>,
    // ...
  |},
  // ...
|};

It will allow to implement your particular check as:

const diff = diffSchema(oldSchema, newSchema);

for (const newType of diff.added.types) {
  if (newType.description) {
    console.log('Description of a new type ...');
  }
  newType.getFields().forEach(reportNewField);
}

for (const typeDiff of diff.changed.types) {
  if (typeDiff.added.description) {
    console.log('Added type description ...');
  }
  typeDiff.added.fields.forEach(reportNewField);
  if (typeDiff.changed.description) {
    console.log('Changed type description ...');
  }
  for (const fieldDiff of typeDiff.changed.field) {
    if (fieldDiff.added.description) {
      console.log('Added field description ...');
      // ...
    }
  }
}

function reportNewField(field) {
  if (field.description) {
    // ...
  }
}

I think it would be more generic approach and allow other developers to easily implement their project-specific needs.

@leebyron What do you think?

@eapache
Copy link
Member Author

eapache commented Apr 2, 2018

Different developers and teams have different needs but we can't add find*Changes function for every case. So I think we need more generic solutions for finding the difference between two schemas.

You're not wrong, however a full diffSchema is well outside of my available time here (and probably outside of my skillset to implement correctly in js). If you'd prefer to move in that direction instead I'll wait until somebody else tackles it and we can keep just using our copy of this method internally for the time being.

The only thing I can think might still belong outside of diffSchema is detecting breaking changes, since to a certain extent that check is not just descriptive but also prescriptive of what changes count as breaking. Though if that is already well-specified elsewhere we could just have a breaking: true/false flag on the various different types returned by diffSchema?

Moreover, some documentation teams would be interested in being notified of new types without description.

Also a good point. We just have CI fail in this case, but I imagine that isn't universal.

@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 [email protected]. Thanks!

@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!

@IvanGoncharov
Copy link
Member

Closing since it's outdated but extracted diffSchema idea into #3224

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