diff --git a/fixtures/graphql/introspection.graphql b/fixtures/graphql/introspection.graphql index d8d20b6..c5a8f43 100644 --- a/fixtures/graphql/introspection.graphql +++ b/fixtures/graphql/introspection.graphql @@ -20,15 +20,27 @@ query IntrospectionQuery($typeName: String!) { kind name description + ofType { + name + description + } } } interfaces { name description + ofType { + name + description + } } possibleTypes { name description + ofType { + name + description + } } enumValues(includeDeprecated: true) { name @@ -43,11 +55,19 @@ query IntrospectionQuery($typeName: String!) { kind name description + ofType { + name + description + } } } ofType { name description + ofType { + name + description + } } } } diff --git a/package.json b/package.json index 6f3897b..bd68eaf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@jahia/cypress", - "version": "3.17.8", + "version": "3.17.9", "scripts": { "build": "tsc", "lint": "eslint src -c .eslintrc.json --ext .ts" diff --git a/src/utils/GraphQLHelper.ts b/src/utils/GraphQLHelper.ts index fd2e78a..0aeeed9 100644 --- a/src/utils/GraphQLHelper.ts +++ b/src/utils/GraphQLHelper.ts @@ -9,14 +9,31 @@ interface GraphQLDescription { description: string; // Description of the element in the schema schemaType: string; // Type of the element in the schema according to the GraphQL spec schemaNode: GraphQLField; // The actual node fetched from the schema - nodePath: string[] // The path to the node in the schema + nodePath: string[]; // The path to the node in the schema + isDeprecated?: boolean; + deprecationReason?: string; } +// The type name to be used for further introspection can be located either in +// the type object itself or in the ofType object +const addTypes = ((type, atPath) => { + const newTypes = []; + if (type.name !== null) { + newTypes.push({typeName: type.name, atPath: atPath}); + } + + if (type.ofType && type.ofType.name !== null) { + newTypes.push({typeName: type.ofType.name, atPath: atPath}); + } + + return newTypes; +}); + // This returns a flat list of all children fields and args for a given GraphQL node // This list can then be used in Cypress tests to look for missing descriptions export const getDescriptions = (rootNode: string): Cypress.Chainable => { cy.log('Starting analysis from GraphQL node: ' + rootNode); - return execIntrospection(rootNode, [], []).then(descriptions => { + return execIntrospection(rootNode, [], [rootNode]).then(descriptions => { return descriptions; }); }; @@ -36,7 +53,7 @@ const execIntrospection = (typeName: string, descriptions: GraphQLDescription[], if (responseDataType) { // This array will be populated with types identified in the introspection query // These will then be further introspected to get their children fields and args - const fetchSubTypes: {typeName: string, atPath: string[]}[] = []; + let fetchSubTypes: {typeName: string, atPath: string[]}[] = []; descriptions.push({ name: responseDataType.name, @@ -50,15 +67,17 @@ const execIntrospection = (typeName: string, descriptions: GraphQLDescription[], // spec available at https://github.com/graphql/graphql-spec/blob/main/spec/Section%204%20--%20Introspection.md if (responseDataType.fields) { for (const graphqlField of responseDataType.fields) { - const fieldPath = [...nodePath, responseDataType.name, graphqlField.name]; + const fieldPath = [...nodePath, graphqlField.name]; descriptions.push({ name: graphqlField.name, description: graphqlField.description, schemaType: '__Field', schemaNode: graphqlField, + isDeprecated: graphqlField.isDeprecated, + deprecationReason: graphqlField.deprecationReason, nodePath: fieldPath }); - fetchSubTypes.push({typeName: graphqlField.type.name, atPath: fieldPath}); + fetchSubTypes = [...fetchSubTypes, ...addTypes(graphqlField.type, fieldPath)]; if (graphqlField.args) { for (const graphQLInputValue of graphqlField.args) { @@ -68,9 +87,11 @@ const execIntrospection = (typeName: string, descriptions: GraphQLDescription[], description: graphQLInputValue.description, schemaType: '__InputValue', schemaNode: graphQLInputValue, + isDeprecated: graphQLInputValue.isDeprecated, + deprecationReason: graphQLInputValue.deprecationReason, nodePath: inputValuePath }); - fetchSubTypes.push({typeName: graphQLInputValue.type.name, atPath: inputValuePath}); + fetchSubTypes = [...fetchSubTypes, ...addTypes(graphQLInputValue.type, inputValuePath)]; } } } @@ -78,15 +99,14 @@ const execIntrospection = (typeName: string, descriptions: GraphQLDescription[], if (responseDataType.interfaces) { for (const graphQLInterfaceType of responseDataType.interfaces) { - const fieldPath = [...nodePath, responseDataType.name, graphQLInterfaceType.name]; descriptions.push({ name: graphQLInterfaceType.name, description: graphQLInterfaceType.description, schemaType: '__Type', schemaNode: graphQLInterfaceType, - nodePath: fieldPath + nodePath }); - fetchSubTypes.push({typeName: graphQLInterfaceType.name, atPath: fieldPath}); + fetchSubTypes = [...fetchSubTypes, ...addTypes(graphQLInterfaceType, nodePath)]; } } @@ -100,7 +120,7 @@ const execIntrospection = (typeName: string, descriptions: GraphQLDescription[], schemaNode: graphQLType, nodePath: fieldPath }); - fetchSubTypes.push({typeName: graphQLType.name, atPath: fieldPath}); + fetchSubTypes = [...fetchSubTypes, ...addTypes(graphQLType, fieldPath)]; } } @@ -112,7 +132,9 @@ const execIntrospection = (typeName: string, descriptions: GraphQLDescription[], description: graphQLEnumValue.description, schemaType: '__EnumValue', schemaNode: graphQLEnumValue, - nodePath: enumPath + nodePath: enumPath, + isDeprecated: graphQLEnumValue.isDeprecated, + deprecationReason: graphQLEnumValue.deprecationReason }); } } @@ -127,12 +149,12 @@ const execIntrospection = (typeName: string, descriptions: GraphQLDescription[], schemaNode: graphQLInputValue, nodePath: inputValuePath }); - fetchSubTypes.push({typeName: graphQLInputValue.type.name, atPath: inputValuePath}); + fetchSubTypes = [...fetchSubTypes, ...addTypes(graphQLInputValue.type, inputValuePath)]; } } if (responseDataType.ofType) { - fetchSubTypes.push({typeName: responseDataType.ofType.name, atPath: nodePath}); + fetchSubTypes = [...fetchSubTypes, ...addTypes(responseDataType.ofType, nodePath)]; } const uniqueSubTypes = fetchSubTypes @@ -143,6 +165,11 @@ const execIntrospection = (typeName: string, descriptions: GraphQLDescription[], // Remove types that might have already been introspected .filter(subtype => descriptions.find(d => d.schemaType === '__Type' && d.name === subtype.typeName) === undefined); + // If there are no subtypes to introspect, we still need to return the descriptions + if (uniqueSubTypes.length === 0) { + return descriptions; + } + return Cypress.Promise.each(uniqueSubTypes, subType => { return execIntrospection(subType.typeName, descriptions, [...subType.atPath, subType.typeName]); }).then(() => descriptions); // Return descriptions after all recursive calls have completed