|
|
@@ -0,0 +1,174 @@
|
|
|
+import fs from 'fs';
|
|
|
+import {
|
|
|
+ buildClientSchema,
|
|
|
+ GraphQLField,
|
|
|
+ GraphQLInputObjectType,
|
|
|
+ GraphQLNamedType,
|
|
|
+ GraphQLObjectType,
|
|
|
+ GraphQLType,
|
|
|
+ isEnumType,
|
|
|
+ isInputObjectType,
|
|
|
+ isNamedType,
|
|
|
+ isObjectType,
|
|
|
+ isScalarType,
|
|
|
+} from 'graphql';
|
|
|
+import path from 'path';
|
|
|
+
|
|
|
+import { deleteGeneratedDocs, generateFrontMatter } from './docgen-utils';
|
|
|
+
|
|
|
+// tslint:disable:no-console
|
|
|
+
|
|
|
+// The path to the introspection schema json file
|
|
|
+const SCHEMA_FILE = path.join(__dirname, '../schema.json');
|
|
|
+// The absolute URL to the generated api docs section
|
|
|
+const docsUrl = '/docs/graphql-api/';
|
|
|
+// The directory in which the markdown files will be saved
|
|
|
+const outputPath = path.join(__dirname, '../docs/content/docs/graphql-api');
|
|
|
+
|
|
|
+const enum FileName {
|
|
|
+ ENUM = 'enums',
|
|
|
+ INPUT = 'input-types',
|
|
|
+ MUTATION = 'mutations',
|
|
|
+ QUERY = 'queries',
|
|
|
+ OBJECT = 'object-types',
|
|
|
+}
|
|
|
+
|
|
|
+const schemaJson = fs.readFileSync(SCHEMA_FILE, 'utf8');
|
|
|
+const parsed = JSON.parse(schemaJson);
|
|
|
+const schema = buildClientSchema(parsed.data);
|
|
|
+
|
|
|
+deleteGeneratedDocs(outputPath);
|
|
|
+generateApiDocs(outputPath);
|
|
|
+
|
|
|
+function generateApiDocs(hugoOutputPath: string) {
|
|
|
+ const timeStart = +new Date();
|
|
|
+ let queriesOutput = generateFrontMatter('Queries', 1) + `\n\n# Queries\n\n`;
|
|
|
+ let mutationsOutput = generateFrontMatter('Mutations', 2) + `\n\n# Mutations\n\n`;
|
|
|
+ let objectTypesOutput = generateFrontMatter('Types', 3) + `\n\n# Types\n\n`;
|
|
|
+ let inputTypesOutput = generateFrontMatter('Input Objects', 4) + `\n\n# Input Objects\n\n`;
|
|
|
+ let enumsOutput = generateFrontMatter('Enums', 5) + `\n\n# Enums\n\n`;
|
|
|
+
|
|
|
+ for (const type of Object.values(schema.getTypeMap())) {
|
|
|
+ if (type.name.substring(0, 2) === '__') {
|
|
|
+ // ignore internal types
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isObjectType(type)) {
|
|
|
+ if (type.name === 'Query') {
|
|
|
+ for (const field of Object.values(type.getFields())) {
|
|
|
+ queriesOutput += `## ${field.name}\n`;
|
|
|
+ queriesOutput += renderDescription(field);
|
|
|
+ queriesOutput += renderFields([field], false) + '\n\n';
|
|
|
+ }
|
|
|
+ } else if (type.name === 'Mutation') {
|
|
|
+ for (const field of Object.values(type.getFields())) {
|
|
|
+ mutationsOutput += `## ${field.name}\n`;
|
|
|
+ mutationsOutput += renderDescription(field);
|
|
|
+ mutationsOutput += renderFields([field], false) + '\n\n';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ objectTypesOutput += `## ${type.name}\n\n`;
|
|
|
+ objectTypesOutput += renderDescription(type);
|
|
|
+ objectTypesOutput += renderFields(type);
|
|
|
+ objectTypesOutput += `\n`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isEnumType(type)) {
|
|
|
+ enumsOutput += `## ${type.name}\n\n`;
|
|
|
+ enumsOutput += renderDescription(type) + '\n\n';
|
|
|
+ enumsOutput += '{{% gql-enum-values %}}\n';
|
|
|
+ for (const value of type.getValues()) {
|
|
|
+ enumsOutput += value.description ? ` * *// ${value.description.trim()}*\n` : '';
|
|
|
+ enumsOutput += ` * ${value.name}\n`;
|
|
|
+ }
|
|
|
+ enumsOutput += '{{% /gql-enum-values %}}\n';
|
|
|
+ enumsOutput += '\n';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isScalarType(type)) {
|
|
|
+ objectTypesOutput += `## ${type.name}\n\n`;
|
|
|
+ objectTypesOutput += renderDescription(type);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isInputObjectType(type)) {
|
|
|
+ inputTypesOutput += `## ${type.name}\n\n`;
|
|
|
+ inputTypesOutput += renderDescription(type);
|
|
|
+ inputTypesOutput += renderFields(type);
|
|
|
+ inputTypesOutput += `\n`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fs.writeFileSync(path.join(hugoOutputPath, FileName.QUERY + '.md'), queriesOutput);
|
|
|
+ fs.writeFileSync(path.join(hugoOutputPath, FileName.MUTATION + '.md'), mutationsOutput);
|
|
|
+ fs.writeFileSync(path.join(hugoOutputPath, FileName.OBJECT + '.md'), objectTypesOutput);
|
|
|
+ fs.writeFileSync(path.join(hugoOutputPath, FileName.INPUT + '.md'), inputTypesOutput);
|
|
|
+ fs.writeFileSync(path.join(hugoOutputPath, FileName.ENUM + '.md'), enumsOutput);
|
|
|
+
|
|
|
+ console.log(`Generated 5 GraphQL API docs in ${+new Date() - timeStart}ms`);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Renders the type description if it exists.
|
|
|
+ */
|
|
|
+function renderDescription(type: { description?: string | null }, appendNewlines = true): string {
|
|
|
+ return type.description ? `${type.description + (appendNewlines ? '\n\n' : '')}` : '';
|
|
|
+}
|
|
|
+
|
|
|
+function renderFields(
|
|
|
+ typeOrFields: (GraphQLObjectType | GraphQLInputObjectType) | Array<GraphQLField<any, any>>,
|
|
|
+ includeDescription = true,
|
|
|
+): string {
|
|
|
+ let output = '{{% gql-fields %}}\n';
|
|
|
+ const fieldsArray: Array<GraphQLField<any, any>> = Array.isArray(typeOrFields)
|
|
|
+ ? typeOrFields
|
|
|
+ : Object.values(typeOrFields.getFields());
|
|
|
+ for (const field of fieldsArray) {
|
|
|
+ if (includeDescription) {
|
|
|
+ output += field.description ? `* *// ${field.description.trim()}*\n` : '';
|
|
|
+ }
|
|
|
+ output += ` * ${renderFieldSignature(field)}\n`;
|
|
|
+ }
|
|
|
+ output += '{{% /gql-fields %}}\n\n';
|
|
|
+ return output;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Renders a field signature including any argument and output type
|
|
|
+ */
|
|
|
+function renderFieldSignature(field: GraphQLField<any, any>): string {
|
|
|
+ let name = field.name;
|
|
|
+ if (field.args && field.args.length) {
|
|
|
+ name += `(${field.args.map(arg => arg.name + ': ' + renderTypeAsLink(arg.type)).join(', ')})`;
|
|
|
+ }
|
|
|
+ return `${name}: ${renderTypeAsLink(field.type)}`;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Renders a type as a markdown link.
|
|
|
+ */
|
|
|
+function renderTypeAsLink(type: GraphQLType): string {
|
|
|
+ const innerType = unwrapType(type);
|
|
|
+ const fileName = isEnumType(innerType)
|
|
|
+ ? FileName.ENUM
|
|
|
+ : isInputObjectType(innerType)
|
|
|
+ ? FileName.INPUT
|
|
|
+ : FileName.OBJECT;
|
|
|
+ const url = `${docsUrl}${fileName}#${innerType.name.toLowerCase()}`;
|
|
|
+ return type.toString().replace(innerType.name, `[${innerType.name}](${url})`);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Unwraps the inner type from a higher-order type, e.g. [Address!]! => Address
|
|
|
+ */
|
|
|
+function unwrapType(type: GraphQLType): GraphQLNamedType {
|
|
|
+ if (isNamedType(type)) {
|
|
|
+ return type;
|
|
|
+ }
|
|
|
+ let innerType = type;
|
|
|
+ while (!isNamedType(innerType)) {
|
|
|
+ innerType = innerType.ofType;
|
|
|
+ }
|
|
|
+ return innerType;
|
|
|
+}
|