UNPKG

graphql-language-service-utils

Version:
294 lines (267 loc) 8.04 kB
/** * Copyright (c) 2021 GraphQL Contributors. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import { GraphQLInputField, GraphQLInputType, isEnumType, isInputObjectType, isListType, isNonNullType, isScalarType, } from 'graphql'; import type { JSONSchema4Type, JSONSchema6, JSONSchema6Definition, JSONSchema6TypeName, } from 'json-schema'; import type { VariableToType } from './collectVariables'; export type { JSONSchema6, JSONSchema6TypeName }; export type JsonSchemaOptions = { /** * use undocumented `monaco-json` `markdownDescription` field in place of json-schema spec `description` field. */ useMarkdownDescription?: boolean; }; type PropertiedJSON6 = JSONSchema6 & { properties: { [k: string]: JSONSchema6; }; }; export type JSONSchemaOptions = { /** * whether to append a non-json schema valid 'markdownDescription` for `monaco-json` */ useMarkdownDescription?: boolean; }; export const defaultJSONSchemaOptions = { useMarkdownDescription: false, }; export type MonacoEditorJSONSchema = JSONSchema6 & { markdownDescription?: string; }; export type CombinedSchema = JSONSchema6 | MonacoEditorJSONSchema; type Definitions = { [k: string]: JSONSchema6Definition }; export type DefinitionResult = { definition: JSONSchema6 | MonacoEditorJSONSchema; required: boolean; definitions?: Definitions; }; function text(into: string[], newText: string) { into.push(newText); } function renderType(into: string[], t: GraphQLInputType | GraphQLInputField) { if (isNonNullType(t)) { renderType(into, t.ofType); text(into, '!'); } else if (isListType(t)) { text(into, '['); // @ts-ignore renderType(into, t.ofType); text(into, ']'); } else { text(into, t.name); } } function renderTypeToString( t: GraphQLInputType | GraphQLInputField, useMarkdown?: boolean, ) { const into: string[] = []; if (useMarkdown) { text(into, '```graphql\n'); } renderType(into, t); if (useMarkdown) { text(into, '\n```'); } return into.join(''); } const scalarTypesMap: { [key: string]: JSONSchema6TypeName } = { Int: 'integer', String: 'string', Float: 'number', ID: 'string', Boolean: 'boolean', // { "type": "string", "format": "date" } is not compatible with proposed DateTime GraphQL-Scalars.com spec DateTime: 'string', }; /** * * @param type {GraphQLInputType} * @returns {DefinitionResult} */ function getJSONSchemaFromGraphQLType( type: GraphQLInputType | GraphQLInputField, options?: JSONSchemaOptions, ): DefinitionResult { let required = false; let definition: CombinedSchema = Object.create(null); const definitions: Definitions = Object.create(null); // TODO: test that this works? if ('defaultValue' in type && type.defaultValue !== undefined) { definition.default = type.defaultValue as JSONSchema4Type | undefined; } if (isEnumType(type)) { definition.type = 'string'; definition.enum = type.getValues().map(val => val.name); } if (isScalarType(type)) { // I think this makes sense for custom scalars? definition.type = scalarTypesMap[type.name] ?? 'any'; } if (isListType(type)) { definition.type = 'array'; const { definition: def, definitions: defs } = getJSONSchemaFromGraphQLType( type.ofType, options, ); if (def.$ref) { definition.items = { $ref: def.$ref }; } else { definition.items = def; } if (defs) { Object.keys(defs).forEach(defName => { definitions[defName] = defs[defName]; }); } } if (isNonNullType(type)) { required = true; const { definition: def, definitions: defs } = getJSONSchemaFromGraphQLType( type.ofType, options, ); definition = def; if (defs) { Object.keys(defs).forEach(defName => { definitions[defName] = defs[defName]; }); } } if (isInputObjectType(type)) { definition.$ref = `#/definitions/${type.name}`; const fields = type.getFields(); const fieldDef: PropertiedJSON6 = { type: 'object', properties: {}, required: [], }; if (type.description) { fieldDef.description = type.description + `\n` + renderTypeToString(type); if (options?.useMarkdownDescription) { // @ts-expect-error fieldDef.markdownDescription = type.description + `\n` + renderTypeToString(type, true); } } else { fieldDef.description = renderTypeToString(type); if (options?.useMarkdownDescription) { // @ts-expect-error fieldDef.markdownDescription = renderTypeToString(type, true); } } Object.keys(fields).forEach(fieldName => { const field = fields[fieldName]; const { required: fieldRequired, definition: typeDefinition, definitions: typeDefinitions, } = getJSONSchemaFromGraphQLType(field.type, options); const { definition: fieldDefinition, // definitions: fieldDefinitions, } = getJSONSchemaFromGraphQLType(field, options); fieldDef.properties[fieldName] = { ...typeDefinition, ...fieldDefinition, } as JSONSchema6; const renderedField = renderTypeToString(field.type); fieldDef.properties[fieldName].description = field.description ? field.description + '\n' + renderedField : renderedField; if (options?.useMarkdownDescription) { const renderedFieldMarkdown = renderTypeToString(field.type, true); fieldDef.properties[ fieldName // @ts-expect-error ].markdownDescription = field.description ? field.description + '\n' + renderedFieldMarkdown : renderedFieldMarkdown; } if (fieldRequired) { fieldDef.required!.push(fieldName); } if (typeDefinitions) { Object.keys(typeDefinitions).map(defName => { definitions[defName] = typeDefinitions[defName]; }); } }); definitions![type.name] = fieldDef; } // append descriptions if ( 'description' in type && !isScalarType(type) && type.description && !definition.description ) { definition.description = type.description + '\n' + renderTypeToString(type); if (options?.useMarkdownDescription) { // @ts-expect-error definition.markdownDescription = type.description + '\n' + renderTypeToString(type, true); } } else { definition.description = renderTypeToString(type); if (options?.useMarkdownDescription) { // @ts-expect-error definition.markdownDescription = renderTypeToString(type, true); } } return { required, definition, definitions }; } /** * Generate a JSONSchema6 valid document from a map of Map<string, GraphQLInputDefinition> * * TODO: optimize with shared definitions. * Otherwise, if you have multiple variables in your operations with the same input type, they are repeated. * * @param facts {OperationFacts} the result of getOperationFacts, or getOperationASTFacts * @returns {JSONSchema6} */ export function getVariablesJSONSchema( variableToType: VariableToType, options?: JSONSchemaOptions, ): JSONSchema6 { const jsonSchema: PropertiedJSON6 = { $schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object', properties: {}, required: [], }; if (variableToType) { // I would use a reduce here, but I wanted it to be readable. Object.entries(variableToType).forEach(([variableName, type]) => { const { definition, required, definitions, } = getJSONSchemaFromGraphQLType(type, options); jsonSchema.properties[variableName] = definition; if (required) { jsonSchema.required?.push(variableName); } if (definitions) { jsonSchema.definitions = { ...jsonSchema?.definitions, ...definitions }; } }); } return jsonSchema; }