Skip to content

Commit

Permalink
TECH-1631: Added a utility function to fetch GraphQL descriptions fro…
Browse files Browse the repository at this point in the history
…m all nodes of the schema (#64)
  • Loading branch information
Fgerthoffert authored Feb 8, 2024
1 parent 37e0430 commit 37756f8
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 0 deletions.
53 changes: 53 additions & 0 deletions fixtures/graphql/introspection.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
query IntrospectionQuery($typeName: String!) {
__type(name: $typeName) {
kind
name
description
fields(includeDeprecated: true) {
name
description
deprecationReason
isDeprecated
args {
name
description
type {
kind
name
}
}
type {
kind
name
description
}
}
interfaces {
name
description
}
possibleTypes {
name
description
}
enumValues(includeDeprecated: true) {
name
description
deprecationReason
isDeprecated
}
inputFields {
name
description
type {
kind
name
description
}
}
ofType {
name
description
}
}
}
152 changes: 152 additions & 0 deletions src/utils/GraphQLHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/* eslint max-depth: ["error", 5] */

interface GraphQLField {
name: string;
}

interface GraphQLDescription {
name: string; // Name of the element in the schema
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
}

// 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 descriptions;
});
};

const execIntrospection = (typeName: string, descriptions: GraphQLDescription[], nodePath): Cypress.Chainable => {
return cy.apollo({
variables: {
typeName: typeName
},
queryFile: 'graphql/introspection.graphql'
}).then(response => {
const responseDataType = response?.data?.__type;
if (responseDataType === null || responseDataType === undefined || responseDataType.kind === 'UNION') {
return;
}

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[]}[] = [];

descriptions.push({
name: responseDataType.name,
description: responseDataType.description,
schemaType: '__Type',
schemaNode: responseDataType,
nodePath
});

// The following exploration of the object follows precisely the Graphql Introspection
// 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];
descriptions.push({
name: graphqlField.name,
description: graphqlField.description,
schemaType: '__Field',
schemaNode: graphqlField,
nodePath: fieldPath
});
fetchSubTypes.push({typeName: graphqlField.type.name, atPath: fieldPath});

if (graphqlField.args) {
for (const graphQLInputValue of graphqlField.args) {
const inputValuePath = [...fieldPath, graphQLInputValue.name];
descriptions.push({
name: graphQLInputValue.name,
description: graphQLInputValue.description,
schemaType: '__InputValue',
schemaNode: graphQLInputValue,
nodePath: inputValuePath
});
fetchSubTypes.push({typeName: graphQLInputValue.type.name, atPath: inputValuePath});
}
}
}
}

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
});
fetchSubTypes.push({typeName: graphQLInterfaceType.name, atPath: fieldPath});
}
}

if (responseDataType.possibleTypes) {
for (const graphQLType of responseDataType.possibleTypes) {
const fieldPath = [...nodePath, responseDataType.name, graphQLType.name];
descriptions.push({
name: graphQLType.name,
description: graphQLType.description,
schemaType: '__Type',
schemaNode: graphQLType,
nodePath: fieldPath
});
fetchSubTypes.push({typeName: graphQLType.name, atPath: fieldPath});
}
}

if (responseDataType.enumValues) {
for (const graphQLEnumValue of responseDataType.enumValues) {
const enumPath = [...nodePath, responseDataType.name, graphQLEnumValue.name];
descriptions.push({
name: graphQLEnumValue.name,
description: graphQLEnumValue.description,
schemaType: '__EnumValue',
schemaNode: graphQLEnumValue,
nodePath: enumPath
});
}
}

if (responseDataType.inputFields) {
for (const graphQLInputValue of responseDataType.inputFields) {
const inputValuePath = [...nodePath, responseDataType.name, graphQLInputValue.name];
descriptions.push({
name: graphQLInputValue.name,
description: graphQLInputValue.description,
schemaType: '__InputValue',
schemaNode: graphQLInputValue,
nodePath: inputValuePath
});
fetchSubTypes.push({typeName: graphQLInputValue.type.name, atPath: inputValuePath});
}
}

if (responseDataType.ofType) {
fetchSubTypes.push({typeName: responseDataType.ofType.name, atPath: nodePath});
}

const uniqueSubTypes = fetchSubTypes
// Filter out duplicate types to ensure we don't introspect the same type multiple times
.filter((obj, index) => fetchSubTypes.findIndex(item => item.typeName === obj.typeName) === index)
// Filter out types that have a name of null (e.g. List of non-null types)
.filter(subtype => subtype.typeName !== null)
// Remove types that might have already been introspected
.filter(subtype => descriptions.find(d => d.schemaType === '__Type' && d.name === subtype.typeName) === undefined);

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

1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './UsersHelper';
export * from './VanityUrlHelper';
export * from './ClusterHelper';
export * from './JahiaPlatformHelper';
export * from './GraphQLHelper';

0 comments on commit 37756f8

Please sign in to comment.