diff --git a/src/utilities/__tests__/findDescriptionChanges-test.js b/src/utilities/__tests__/findDescriptionChanges-test.js new file mode 100644 index 0000000000..d3aec17ab6 --- /dev/null +++ b/src/utilities/__tests__/findDescriptionChanges-test.js @@ -0,0 +1,140 @@ +/** + * Copyright (c) 2016, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +import { expect } from 'chai'; +import { describe, it } from 'mocha'; +import { GraphQLObjectType, GraphQLSchema, GraphQLString } from '../../type'; +import { findDescriptionChanges } from '../findDescriptionChanges'; + +describe('findDescriptionChanges', () => { + const queryType = new GraphQLObjectType({ + name: 'Query', + fields: { + field1: { type: GraphQLString }, + }, + }); + + it('should detect if a description was added to a type', () => { + const typeOld = new GraphQLObjectType({ + name: 'Type', + fields: { + field1: { type: GraphQLString }, + }, + }); + const typeNew = new GraphQLObjectType({ + name: 'Type', + description: 'Something rather', + fields: { + field1: { type: GraphQLString }, + }, + }); + + const oldSchema = new GraphQLSchema({ + query: queryType, + types: [typeOld], + }); + const newSchema = new GraphQLSchema({ + query: queryType, + types: [typeNew], + }); + expect(findDescriptionChanges(oldSchema, newSchema)[0].description).to.eql( + 'Description added on TYPE Type.', + ); + expect(findDescriptionChanges(oldSchema, oldSchema)).to.eql([]); + expect(findDescriptionChanges(newSchema, newSchema)).to.eql([]); + }); + + it('should detect if a description was changed on a type', () => { + const typeOld = new GraphQLObjectType({ + name: 'Type', + description: 'Something rather', + fields: { + field1: { type: GraphQLString }, + }, + }); + const typeNew = new GraphQLObjectType({ + name: 'Type', + description: 'Something else', + fields: { + field1: { type: GraphQLString }, + }, + }); + + const oldSchema = new GraphQLSchema({ + query: queryType, + types: [typeOld], + }); + const newSchema = new GraphQLSchema({ + query: queryType, + types: [typeNew], + }); + expect(findDescriptionChanges(oldSchema, newSchema)[0].description).to.eql( + 'Description changed on TYPE Type.', + ); + expect(findDescriptionChanges(oldSchema, oldSchema)).to.eql([]); + expect(findDescriptionChanges(newSchema, newSchema)).to.eql([]); + }); + + it('should detect if a type with a description was added', () => { + const type = new GraphQLObjectType({ + name: 'Type', + description: 'Something rather', + fields: { + field1: { type: GraphQLString }, + }, + }); + + const oldSchema = new GraphQLSchema({ + query: queryType, + types: [], + }); + const newSchema = new GraphQLSchema({ + query: queryType, + types: [type], + }); + expect(findDescriptionChanges(oldSchema, newSchema)[0].description).to.eql( + 'New TYPE Type added with description.', + ); + expect(findDescriptionChanges(oldSchema, oldSchema)).to.eql([]); + expect(findDescriptionChanges(newSchema, newSchema)).to.eql([]); + }); + + it('should detect if a field with a description was added', () => { + const typeOld = new GraphQLObjectType({ + name: 'Type', + fields: { + field1: { type: GraphQLString }, + }, + }); + const typeNew = new GraphQLObjectType({ + name: 'Type', + fields: { + field1: { type: GraphQLString }, + field2: { + type: GraphQLString, + description: 'Something rather', + }, + }, + }); + + const oldSchema = new GraphQLSchema({ + query: queryType, + types: [typeOld], + }); + const newSchema = new GraphQLSchema({ + query: queryType, + types: [typeNew], + }); + expect(findDescriptionChanges(oldSchema, newSchema)[0].description).to.eql( + 'New FIELD field2 added with description.', + ); + expect(findDescriptionChanges(oldSchema, oldSchema)).to.eql([]); + expect(findDescriptionChanges(newSchema, newSchema)).to.eql([]); + }); +}); diff --git a/src/utilities/findDescriptionChanges.js b/src/utilities/findDescriptionChanges.js new file mode 100644 index 0000000000..64eb4f9893 --- /dev/null +++ b/src/utilities/findDescriptionChanges.js @@ -0,0 +1,163 @@ +/* eslint-disable no-restricted-syntax */ +// @flow + +import { + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLEnumType, + GraphQLInputObjectType, +} from '../type/definition'; +import type { GraphQLFieldMap } from '../type/definition'; +import { GraphQLSchema } from '../type/schema'; +import invariant from '../jsutils/invariant'; + +export const DescribedObjectType = { + FIELD: 'FIELD', + TYPE: 'TYPE', + ARGUMENT: 'ARGUMENT', + ENUM_VALUE: 'ENUM_VALUE', +}; + +export const DescriptionChangeType = { + OBJECT_ADDED: 'OBJECT_ADDED', + DESCRIPTION_ADDED: 'DESCRIPTION_ADDED', + DESCRIPTION_CHANGED: 'DESCRIPTION_CHANGED', +}; + +export type DescriptionChange = { + object: $Keys, + change: $Keys, + description: string, + oldThing: any, + newThing: any, +}; + +/** + * Given two schemas, returns an Array containing descriptions of any + * descriptions that are new or changed and need review. + */ +export function findDescriptionChanges( + oldSchema: GraphQLSchema, + newSchema: GraphQLSchema, +): Array { + const oldTypeMap = oldSchema.getTypeMap(); + const newTypeMap = newSchema.getTypeMap(); + + const descriptionChanges: Array = []; + + Object.keys(newTypeMap).forEach(typeName => { + const oldType = oldTypeMap[typeName]; + const newType = newTypeMap[typeName]; + + descriptionChanges.push( + generateDescriptionChange(newType, oldType, DescribedObjectType.TYPE), + ); + + if ( + newType instanceof GraphQLObjectType || + newType instanceof GraphQLInterfaceType || + newType instanceof GraphQLInputObjectType + ) { + invariant( + !oldType || + oldType instanceof GraphQLObjectType || + oldType instanceof GraphQLInterfaceType || + oldType instanceof GraphQLInputObjectType, + 'Expected oldType to also have fields', + ); + const oldTypeFields: ?GraphQLFieldMap<*, *> = oldType + ? oldType.getFields() + : null; + const newTypeFields: GraphQLFieldMap<*, *> = newType.getFields(); + + Object.keys(newTypeFields).forEach(fieldName => { + const oldField = oldTypeFields ? oldTypeFields[fieldName] : null; + const newField = newTypeFields[fieldName]; + + descriptionChanges.push( + generateDescriptionChange( + newField, + oldField, + DescribedObjectType.FIELD, + ), + ); + + if (!newField.args) { + return; + } + + newField.args.forEach(newArg => { + const oldArg = oldField + ? oldField.args.find(arg => arg.name === newArg.name) + : null; + + descriptionChanges.push( + generateDescriptionChange( + newArg, + oldArg, + DescribedObjectType.ARGUMENT, + ), + ); + }); + }); + } else if (newType instanceof GraphQLEnumType) { + invariant( + !oldType || oldType instanceof GraphQLEnumType, + 'Expected oldType to also have values', + ); + const oldValues = oldType ? oldType.getValues() : null; + const newValues = newType.getValues(); + newValues.forEach(newValue => { + const oldValue = oldValues + ? oldValues.find(value => value.name === newValue.name) + : null; + + descriptionChanges.push( + generateDescriptionChange( + newValue, + oldValue, + DescribedObjectType.ENUM_VALUE, + ), + ); + }); + } + }); + + return descriptionChanges.filter(Boolean); +} + +function generateDescriptionChange( + newThing, + oldThing, + objectType: $Keys, +): ?DescriptionChange { + if (!newThing.description) { + return; + } + + if (!oldThing) { + return { + object: objectType, + change: DescriptionChangeType.OBJECT_ADDED, + oldThing, + newThing, + description: `New ${objectType} ${newThing.name} added with description.`, + }; + } else if (!oldThing.description) { + return { + object: objectType, + change: DescriptionChangeType.DESCRIPTION_ADDED, + oldThing, + newThing, + description: `Description added on ${objectType} ${newThing.name}.`, + }; + } else if (oldThing.description !== newThing.description) { + return { + object: objectType, + change: DescriptionChangeType.DESCRIPTION_CHANGED, + oldThing, + newThing, + description: `Description changed on ${objectType} ${newThing.name}.`, + }; + } +}