diff --git a/README.md b/README.md index 36aaf257..b03a35cc 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ module system it is exported as `GraphQLVoyager` global variable. + `displayOptions` _(optional)_ + `displayOptions.skipRelay` [`boolean`, default `true`] - skip relay-related entities + `displayOptions.skipDeprecated` [`boolean`, default `true`] - skip deprecated fields and entities that contain only deprecated fields. + + `displayOptions.skipInterfaceFields` [`boolean`, default `false`] - skip fields that are inherited from interface and have same definition + `displayOptions.rootType` [`string`] - name of the type to be used as a root + `displayOptions.sortByAlphabet` [`boolean`, default `false`] - sort fields on graph by alphabet + `displayOptions.showLeafFields` [`boolean`, default `true`] - show all scalars and enums diff --git a/src/components/Voyager.tsx b/src/components/Voyager.tsx index 209f26c0..51702e0c 100644 --- a/src/components/Voyager.tsx +++ b/src/components/Voyager.tsx @@ -23,6 +23,7 @@ export interface VoyagerDisplayOptions { rootType?: string; skipRelay?: boolean; skipDeprecated?: boolean; + skipInterfaceFields?: boolean; showLeafFields?: boolean; sortByAlphabet?: boolean; hideRoot?: boolean; @@ -32,6 +33,7 @@ const defaultDisplayOptions = { rootType: undefined, skipRelay: true, skipDeprecated: true, + skipInterfaceFields: false, sortByAlphabet: false, showLeafFields: true, hideRoot: false, @@ -130,6 +132,7 @@ export default class Voyager extends React.Component { displayOptions.sortByAlphabet, displayOptions.skipRelay, displayOptions.skipDeprecated, + displayOptions.skipInterfaceFields, ); const typeGraph = getTypeGraph(schema, displayOptions.rootType, displayOptions.hideRoot); diff --git a/src/components/settings/Settings.tsx b/src/components/settings/Settings.tsx index 38398fdd..6c1ed826 100644 --- a/src/components/settings/Settings.tsx +++ b/src/components/settings/Settings.tsx @@ -50,6 +50,13 @@ export default class Settings extends React.Component { onChange={event => onChange({ showLeafFields: event.target.checked })} /> + onChange({ skipInterfaceFields: event.target.checked })} + /> + ); diff --git a/src/graph/dot.ts b/src/graph/dot.ts index dda78296..22711fa2 100644 --- a/src/graph/dot.ts +++ b/src/graph/dot.ts @@ -77,6 +77,7 @@ export function getDot(typeGraph, displayOptions): string { ${objectValues(node.fields, nodeField)} ${possibleTypes(node)} ${derivedTypes(node)} + ${interfacesTypes(node)} > `; } @@ -151,6 +152,26 @@ function derivedTypes(node) { `; } +function interfacesTypes(node) { + const interfacesTypes = node.interfaces; + if (_.isEmpty(interfacesTypes)) { + return ''; + } + return ` + + interfaces + + ${array( + interfacesTypes, + ({ id, type }) => ` + + ${type.name} + + `, + )} + `; +} + function objectValues(object: { [key: string]: X }, stringify: (X) => string): string { return _.values(object) .map(stringify) diff --git a/src/graph/svg-renderer.ts b/src/graph/svg-renderer.ts index e1e1eb90..1cf174a8 100644 --- a/src/graph/svg-renderer.ts +++ b/src/graph/svg-renderer.ts @@ -125,6 +125,12 @@ function preprocessVizSVG(svgString: string) { $possibleType.querySelector('text').classList.add('type-link'); }); + forEachNode(svg, '.interface', $interface => { + // not sure if next line should be here it works without it + // $interface.classList.add('edge-source'); + $interface.querySelector('text').classList.add('type-link'); + }); + const serializer = new XMLSerializer(); return serializer.serializeToString(svg); } diff --git a/src/graph/type-graph.ts b/src/graph/type-graph.ts index b39ed290..dc9b4e2f 100644 --- a/src/graph/type-graph.ts +++ b/src/graph/type-graph.ts @@ -20,6 +20,7 @@ export function getTypeGraph(schema, rootType: string, hideRoot: boolean) { ..._.values(type.fields), ...(type.derivedTypes || []), ...(type.possibleTypes || []), + ...(type.interfaces || []), ]) .map('type') .filter(isNode) diff --git a/src/introspection/introspection.ts b/src/introspection/introspection.ts index 8cadff21..e025e7f9 100644 --- a/src/introspection/introspection.ts +++ b/src/introspection/introspection.ts @@ -138,7 +138,7 @@ function markRelayTypes(schema: SimplifiedIntrospectionWithIds): void { return; } - const edgesType = connectionType.fields.edges.type + const edgesType = connectionType.fields.edges.type; if (edgesType.kind !== 'OBJECT' || !edgesType.fields.node) { return; } @@ -202,6 +202,46 @@ function markDeprecated(schema: SimplifiedIntrospectionWithIds): void { // which are deprecated. } +function markInterfaceFields(schema: SimplifiedIntrospectionWithIds): void { + function isFieldOnInterfaceSameAsOnType(objectField, interfaceField) { + // rules are described here https://graphql.github.io/graphql-spec/draft/#sel-HAHZhCFHABABiCp7I + // we don't need to check for everything as valid rules are enforeced at time of parsing schema + + if (objectField.type.id !== interfaceField.type.id) { + // return type of field is not same as in parrent + // this case also take care of types wrapped in lists or not null modificator + return false; + } + if (objectField.typeWrappers.length !== interfaceField.typeWrappers.length) { + // return type has stricker nullability modificator + // list modifier could not be changed as it would be parse error + return false; + } + if (_.keys(objectField.args).length !== _.keys(interfaceField.args).length) { + // if there are any aditional args they are not same + return false; + } + return true; + } + _.each(schema.types, type => { + if (type.kind === 'OBJECT') { + if (type.interfaces && type.interfaces.length > 0) { + // we have some interfaces for this object + // so we should delete fields that are presented in interface + // and also present in object and are same + + _.each(type.interfaces, oneInterface => { + _.each(oneInterface.type.fields, interfaceField => { + if (isFieldOnInterfaceSameAsOnType(type.fields[interfaceField.name], interfaceField)) { + delete type.fields[interfaceField.name]; + } + }); + }); + } + } + }); +} + function assignTypesAndIDs(schema: SimplifiedIntrospection) { (schema).queryType = schema.types[schema.queryType]; (schema).mutationType = schema.types[schema.mutationType]; @@ -254,6 +294,7 @@ export function getSchema( sortByAlphabet: boolean, skipRelay: boolean, skipDeprecated: boolean, + skipInterfaceFields: boolean, ) { if (!introspection) return null; @@ -273,5 +314,8 @@ export function getSchema( if (skipDeprecated) { markDeprecated((simpleSchema) as SimplifiedIntrospectionWithIds); } + if (skipInterfaceFields) { + markInterfaceFields((simpleSchema) as SimplifiedIntrospectionWithIds); + } return simpleSchema; }