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 IndexGenerationContext['rootType'], }), ); }); 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
; }