import { useAtomValue } from 'jotai'; import { graphqlSchemaAtom } from "../atoms/graphqlSchemaAtom"; import { Input } from "./core/Input"; import type { GraphQLSchema, GraphQLOutputType, GraphQLScalarType, GraphQLField, GraphQLList, GraphQLInputType, GraphQLNonNull, GraphQLObjectType } from "graphql"; import { isNonNullType, isListType } from "graphql"; import { Button } from "./core/Button"; import { useEffect, useState } from 'react'; import { IconButton } from "./core/IconButton"; import { fuzzyFilter } from 'fuzzbunny'; function getRootTypes(graphqlSchema: GraphQLSchema) { return ([ graphqlSchema.getQueryType(), graphqlSchema.getMutationType(), graphqlSchema.getSubscriptionType(), ] .filter(Boolean) as NonNullable>[]) .reduce( ( prev, curr ) => { return { ...prev, [curr.name]: curr, }; }, {} as Record>> ) } function getTypeIndices( type: GraphQLAnyType, context: IndexGenerationContext ): SearchIndexRecord[] { const indices: SearchIndexRecord[] = []; if (!(type as GraphQLObjectType).name) { return indices; } indices.push({ name: (type as GraphQLObjectType).name, type: 'type', schemaPointer: type, args: '' }); if ((type as GraphQLObjectType).getFields) { indices.push( ...getFieldsIndices((type as GraphQLObjectType).getFields(), context) ) } // remove duplicates from index return indices.filter( (x, i, array) => array.findIndex( (y) => y.name === x.name && y.type === x.type ) === i ); } function getFieldsIndices( fieldMap: FieldsMap, context: IndexGenerationContext ): SearchIndexRecord[] { const indices: SearchIndexRecord[] = []; Object.values(fieldMap) .forEach( (field) => { if (!field.name) { return; } const args = field.args && field.args.length > 0 ? field.args.map((arg) => arg.name).join(', ') : ''; indices.push({ name: field.name, type: context.rootType, schemaPointer: field as unknown as Field, args }); if (field.type) { indices.push( ...getTypeIndices(field.type, context) ) } } ); // remove duplicates from index return indices.filter( (x, i, array) => array.findIndex( (y) => y.name === x.name && y.type === x.type ) === i ); } type Field = NonNullable>; type FieldsMap = ReturnType; type GraphQLAnyType = FieldsMap[string]['type']; type SearchIndexRecord = { name: string, args: string, type: 'field' | 'type' | 'Query' | 'Mutation' | 'Subscription', schemaPointer: SchemaPointer }; type IndexGenerationContext = { rootType: 'Query' | 'Mutation' | 'Subscription'; }; type SchemaPointer = Field | GraphQLOutputType | GraphQLInputType | null; type ViewMode = 'explorer' | 'search' | 'field'; type HistoryRecord = { schemaPointer: SchemaPointer, viewMode: ViewMode }; function DocsExplorer({ graphqlSchema }: { graphqlSchema: GraphQLSchema }) { const [rootTypes, setRootTypes] = useState(getRootTypes(graphqlSchema)); const [schemaPointer, setSchemaPointer] = useState(null); const [history, setHistory] = useState([]); const [searchIndex, setSearchIndex] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [viewMode, setViewMode] = useState('explorer'); useEffect(() => { setRootTypes(getRootTypes(graphqlSchema)); }, [graphqlSchema]); useEffect(() => { const typeMap = graphqlSchema.getTypeMap(); const index: SearchIndexRecord[] = Object.values(typeMap) .filter( (x) => !x.name.startsWith('__') ) .map( (x) => ({ name: x.name, type: 'type', schemaPointer: x, args: '' }) ); Object.values(rootTypes) .forEach( (type) => { index.push( ...getFieldsIndices(type.getFields(), { rootType: type.name as any }) ) } ) setSearchIndex( index .filter( (x, i, array) => array.findIndex( (y) => y.name === x.name && y.type === x.type ) === i ) ); }, [graphqlSchema, rootTypes]); useEffect( () => { if (!searchQuery) { setSearchResults([]); return; } const results = fuzzyFilter( searchIndex, searchQuery, { fields: ['name', 'args'] } ) .sort((a, b) => b.score - a.score) .map((v) => v.item); setSearchResults(results); }, [searchIndex, searchQuery] ); const goBack = () => { if (history.length === 0) { return; } const newHistory = history.slice(0, history.length - 1); const prevHistoryRecord = newHistory[newHistory.length - 1]; if (prevHistoryRecord) { const { schemaPointer: newPointer, viewMode } = prevHistoryRecord; setHistory(newHistory); setSchemaPointer(newPointer!); setViewMode(viewMode); return; } goHome(); } const addToHistory = (historyRecord: HistoryRecord) => { setHistory([...history, historyRecord]); } const goHome = () => { setHistory([]); setSchemaPointer(null); setViewMode('explorer'); } const renderRootTypes = () => { return (
{ Object .values(rootTypes) .map( (x) => ( ) ) }
); } const extractActualType = ( type: GraphQLField['type'] | GraphQLInputType ) => { // check if non-null if (isNonNullType(type) || isListType(type)) { return extractActualType((type as GraphQLNonNull).ofType) } return type; } const onTypeClick = ( type: GraphQLField['type'] | GraphQLInputType ) => { // check if non-null if (isNonNullType(type)) { onTypeClick((type as GraphQLNonNull).ofType) return; } // check if list if (isListType(type)) { onTypeClick((type as GraphQLList).ofType); return; } setSchemaPointer(type); addToHistory({ schemaPointer: type as Field, viewMode: 'explorer', }); setViewMode('explorer'); }; const onFieldClick = (field: GraphQLField) => { setSchemaPointer(field as unknown as Field); setViewMode('field'); addToHistory({ schemaPointer: field as unknown as Field, viewMode: 'field', }); }; const renderSubFieldRecord = ( field: FieldsMap[string], options?: { addable?: boolean, } ) => { return (
{ options?.addable ? ( ) : null }
{ " " } {/* Arguments block */ } { field.args && field.args.length > 0 ? ( <> { " " } ( { " " } { field.args.map( (arg, i, array) => ( <> { " " } ) ) } ) ) : null } {/* End of Arguments Block */ } { " " }
{ field.description ? (
{ field.description }
) : null }
); }; const renderScalarField = () => { const scalarField = schemaPointer as GraphQLScalarType; return (
{ scalarField.toConfig().description }
); }; const renderSubFields = () => { if (!schemaPointer) { return null; } if ( !(schemaPointer as Field).getFields ) { // Scalar field return renderScalarField(); } if (!(schemaPointer as Field).getFields()) { return null; } return Object.values((schemaPointer as Field).getFields()) .map( (x) => renderSubFieldRecord(x, { addable: true }) ) }; const renderFieldDocView = () => { if (!schemaPointer) { return null; } return (
{ (schemaPointer as Field).name }
{ (schemaPointer as Field).getFields ? (
Fields
) : null }
{ renderSubFields() }
) } const renderExplorerView = () => { if (history.length === 0) { return renderRootTypes(); } return renderFieldDocView() }; const renderFieldView = () => { if (!schemaPointer) { return null; } const field = schemaPointer as unknown as GraphQLField; const returnType = extractActualType(field.type); return (
{ field.name }
{/* Arguments */} { field.args && field.args.length > 0 ? (
Arguments
{ field.args.map( (arg, i, array) => ( <> { " " } ) ) }
) : null } {/* End of Arguments */} {/* Return type */}
Type
{ returnType.name }
{/* End of Return type */} {/* Fields */} { (returnType as GraphQLObjectType).getFields && Object.values((returnType as GraphQLObjectType).getFields()).length > 0 ? (
Fields
{ Object.values((returnType as GraphQLObjectType).getFields()) .map( (x) => renderSubFieldRecord(x) ) }
) : null } {/* End of Fields */}
); }; const renderTopBar = () => { return (
); }; const renderSearchView = () => { return (
Search results
{ searchResults .map( (result) => ( ) ) }
); }; const renderView = () => { if (viewMode === 'field') { return renderFieldView(); } if (viewMode === 'search') { return renderSearchView(); } return renderExplorerView(); }; return (
{ history.length > 0 || viewMode === 'search' ? renderTopBar() : null }
{/* Search bar */}
{ setSearchQuery(value); } } onKeyDown={ (e) => { // check if enter if (e.key === 'Enter' && viewMode !== 'search') { addToHistory({ schemaPointer: null, viewMode: 'search', }) setViewMode('search'); } } } />
{/* End of search bar */}
{ renderView() }
); } export function GraphQLDocsExplorer() { const graphqlSchema = useAtomValue(graphqlSchemaAtom); if (graphqlSchema) { return ; } return
There is no schema
; }