1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | import {
|
9 | GraphQLInputField,
|
10 | GraphQLInputType,
|
11 | isEnumType,
|
12 | isInputObjectType,
|
13 | isListType,
|
14 | isNonNullType,
|
15 | isScalarType,
|
16 | } from 'graphql';
|
17 |
|
18 | import type {
|
19 | JSONSchema4Type,
|
20 | JSONSchema6,
|
21 | JSONSchema6Definition,
|
22 | JSONSchema6TypeName,
|
23 | } from 'json-schema';
|
24 | import type { VariableToType } from './collectVariables';
|
25 |
|
26 | export type { JSONSchema6, JSONSchema6TypeName };
|
27 |
|
28 | export type JsonSchemaOptions = {
|
29 | |
30 |
|
31 |
|
32 | useMarkdownDescription?: boolean;
|
33 | };
|
34 |
|
35 | type PropertiedJSON6 = JSONSchema6 & {
|
36 | properties: {
|
37 | [k: string]: JSONSchema6;
|
38 | };
|
39 | };
|
40 |
|
41 | export type JSONSchemaOptions = {
|
42 | |
43 |
|
44 |
|
45 | useMarkdownDescription?: boolean;
|
46 | };
|
47 |
|
48 | export const defaultJSONSchemaOptions = {
|
49 | useMarkdownDescription: false,
|
50 | };
|
51 |
|
52 | export type MonacoEditorJSONSchema = JSONSchema6 & {
|
53 | markdownDescription?: string;
|
54 | };
|
55 |
|
56 | export type CombinedSchema = JSONSchema6 | MonacoEditorJSONSchema;
|
57 |
|
58 | type Definitions = { [k: string]: JSONSchema6Definition };
|
59 |
|
60 | export type DefinitionResult = {
|
61 | definition: JSONSchema6 | MonacoEditorJSONSchema;
|
62 | required: boolean;
|
63 | definitions?: Definitions;
|
64 | };
|
65 |
|
66 | function text(into: string[], newText: string) {
|
67 | into.push(newText);
|
68 | }
|
69 |
|
70 | function renderType(into: string[], t: GraphQLInputType | GraphQLInputField) {
|
71 | if (isNonNullType(t)) {
|
72 | renderType(into, t.ofType);
|
73 | text(into, '!');
|
74 | } else if (isListType(t)) {
|
75 | text(into, '[');
|
76 |
|
77 | renderType(into, t.ofType);
|
78 | text(into, ']');
|
79 | } else {
|
80 | text(into, t.name);
|
81 | }
|
82 | }
|
83 |
|
84 | function renderTypeToString(
|
85 | t: GraphQLInputType | GraphQLInputField,
|
86 | useMarkdown?: boolean,
|
87 | ) {
|
88 | const into: string[] = [];
|
89 | if (useMarkdown) {
|
90 | text(into, '```graphql\n');
|
91 | }
|
92 | renderType(into, t);
|
93 | if (useMarkdown) {
|
94 | text(into, '\n```');
|
95 | }
|
96 | return into.join('');
|
97 | }
|
98 |
|
99 | const scalarTypesMap: { [key: string]: JSONSchema6TypeName } = {
|
100 | Int: 'integer',
|
101 | String: 'string',
|
102 | Float: 'number',
|
103 | ID: 'string',
|
104 | Boolean: 'boolean',
|
105 |
|
106 | DateTime: 'string',
|
107 | };
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 | function getJSONSchemaFromGraphQLType(
|
115 | type: GraphQLInputType | GraphQLInputField,
|
116 | options?: JSONSchemaOptions,
|
117 | ): DefinitionResult {
|
118 | let required = false;
|
119 | let definition: CombinedSchema = Object.create(null);
|
120 | const definitions: Definitions = Object.create(null);
|
121 |
|
122 |
|
123 | if ('defaultValue' in type && type.defaultValue !== undefined) {
|
124 | definition.default = type.defaultValue as JSONSchema4Type | undefined;
|
125 | }
|
126 | if (isEnumType(type)) {
|
127 | definition.type = 'string';
|
128 | definition.enum = type.getValues().map(val => val.name);
|
129 | }
|
130 |
|
131 | if (isScalarType(type)) {
|
132 |
|
133 | definition.type = scalarTypesMap[type.name] ?? 'any';
|
134 | }
|
135 | if (isListType(type)) {
|
136 | definition.type = 'array';
|
137 | const { definition: def, definitions: defs } = getJSONSchemaFromGraphQLType(
|
138 | type.ofType,
|
139 | options,
|
140 | );
|
141 | if (def.$ref) {
|
142 | definition.items = { $ref: def.$ref };
|
143 | } else {
|
144 | definition.items = def;
|
145 | }
|
146 | if (defs) {
|
147 | Object.keys(defs).forEach(defName => {
|
148 | definitions[defName] = defs[defName];
|
149 | });
|
150 | }
|
151 | }
|
152 | if (isNonNullType(type)) {
|
153 | required = true;
|
154 | const { definition: def, definitions: defs } = getJSONSchemaFromGraphQLType(
|
155 | type.ofType,
|
156 | options,
|
157 | );
|
158 | definition = def;
|
159 | if (defs) {
|
160 | Object.keys(defs).forEach(defName => {
|
161 | definitions[defName] = defs[defName];
|
162 | });
|
163 | }
|
164 | }
|
165 | if (isInputObjectType(type)) {
|
166 | definition.$ref = `#/definitions/${type.name}`;
|
167 | const fields = type.getFields();
|
168 |
|
169 | const fieldDef: PropertiedJSON6 = {
|
170 | type: 'object',
|
171 | properties: {},
|
172 | required: [],
|
173 | };
|
174 | if (type.description) {
|
175 | fieldDef.description = type.description + `\n` + renderTypeToString(type);
|
176 | if (options?.useMarkdownDescription) {
|
177 |
|
178 | fieldDef.markdownDescription =
|
179 | type.description + `\n` + renderTypeToString(type, true);
|
180 | }
|
181 | } else {
|
182 | fieldDef.description = renderTypeToString(type);
|
183 | if (options?.useMarkdownDescription) {
|
184 |
|
185 | fieldDef.markdownDescription = renderTypeToString(type, true);
|
186 | }
|
187 | }
|
188 |
|
189 | Object.keys(fields).forEach(fieldName => {
|
190 | const field = fields[fieldName];
|
191 | const {
|
192 | required: fieldRequired,
|
193 | definition: typeDefinition,
|
194 | definitions: typeDefinitions,
|
195 | } = getJSONSchemaFromGraphQLType(field.type, options);
|
196 |
|
197 | const {
|
198 | definition: fieldDefinition,
|
199 |
|
200 | } = getJSONSchemaFromGraphQLType(field, options);
|
201 |
|
202 | fieldDef.properties[fieldName] = {
|
203 | ...typeDefinition,
|
204 | ...fieldDefinition,
|
205 | } as JSONSchema6;
|
206 |
|
207 | const renderedField = renderTypeToString(field.type);
|
208 | fieldDef.properties[fieldName].description = field.description
|
209 | ? field.description + '\n' + renderedField
|
210 | : renderedField;
|
211 | if (options?.useMarkdownDescription) {
|
212 | const renderedFieldMarkdown = renderTypeToString(field.type, true);
|
213 | fieldDef.properties[
|
214 | fieldName
|
215 |
|
216 | ].markdownDescription = field.description
|
217 | ? field.description + '\n' + renderedFieldMarkdown
|
218 | : renderedFieldMarkdown;
|
219 | }
|
220 |
|
221 | if (fieldRequired) {
|
222 | fieldDef.required!.push(fieldName);
|
223 | }
|
224 | if (typeDefinitions) {
|
225 | Object.keys(typeDefinitions).map(defName => {
|
226 | definitions[defName] = typeDefinitions[defName];
|
227 | });
|
228 | }
|
229 | });
|
230 | definitions![type.name] = fieldDef;
|
231 | }
|
232 |
|
233 | if (
|
234 | 'description' in type &&
|
235 | !isScalarType(type) &&
|
236 | type.description &&
|
237 | !definition.description
|
238 | ) {
|
239 | definition.description = type.description + '\n' + renderTypeToString(type);
|
240 | if (options?.useMarkdownDescription) {
|
241 |
|
242 | definition.markdownDescription =
|
243 | type.description + '\n' + renderTypeToString(type, true);
|
244 | }
|
245 | } else {
|
246 | definition.description = renderTypeToString(type);
|
247 | if (options?.useMarkdownDescription) {
|
248 |
|
249 | definition.markdownDescription = renderTypeToString(type, true);
|
250 | }
|
251 | }
|
252 |
|
253 | return { required, definition, definitions };
|
254 | }
|
255 |
|
256 |
|
257 |
|
258 |
|
259 |
|
260 |
|
261 |
|
262 |
|
263 |
|
264 | export function getVariablesJSONSchema(
|
265 | variableToType: VariableToType,
|
266 | options?: JSONSchemaOptions,
|
267 | ): JSONSchema6 {
|
268 | const jsonSchema: PropertiedJSON6 = {
|
269 | $schema: 'https://json-schema.org/draft/2020-12/schema',
|
270 | type: 'object',
|
271 | properties: {},
|
272 | required: [],
|
273 | };
|
274 |
|
275 | if (variableToType) {
|
276 |
|
277 | Object.entries(variableToType).forEach(([variableName, type]) => {
|
278 | const {
|
279 | definition,
|
280 | required,
|
281 | definitions,
|
282 | } = getJSONSchemaFromGraphQLType(type, options);
|
283 | jsonSchema.properties[variableName] = definition;
|
284 | if (required) {
|
285 | jsonSchema.required?.push(variableName);
|
286 | }
|
287 | if (definitions) {
|
288 | jsonSchema.definitions = { ...jsonSchema?.definitions, ...definitions };
|
289 | }
|
290 | });
|
291 | }
|
292 | return jsonSchema;
|
293 | }
|