Skip to content

Commit

Permalink
fix: avoid infinity recursiveness in getVariablesJSONSchema (#2917)
Browse files Browse the repository at this point in the history
* fix: avoid infinity recursiveness in getVariablesJSONSchema

add a marker to mark analyzed InputObject to avoid infinity recursiveness while generating JsonSchema.
set $ref field for definition while recursion is found
put marker into options to avoid extra parameters
add test unit for this bug

Co-authored-by: Ted Thibodeau Jr <tthibodeau@openlinksw.com>
  • Loading branch information
woodensail and TallTed committed Jan 21, 2023
1 parent 42f2978 commit f788e65
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 59 deletions.
5 changes: 5 additions & 0 deletions .changeset/fifty-points-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"graphql-language-service": patch
---

Fix infinite recursiveness in getVariablesJSONSchema when the schema contains types that reference themselves
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
schema {
query: query_root
}

input string_options {
_eq: String
_ilike: String
}

type issues {
name: String
}

input issues_where_input {
_and: [issues_where_input!]
name: string_options
}

type query_root {
issues(where: issues_where_input): [issues!]!
}
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,23 @@ describe('getVariablesJSONSchema', () => {
},
});
});

it('should handle recursive schema properly', () => {
const schemaPath = join(__dirname, '__schema__', 'RecursiveSchema.graphql');
schema = buildSchema(readFileSync(schemaPath, 'utf8'));

const variableToType = collectVariables(
schema,
parse(`query Example(
$where: issues_where_input! = {}
) {
issues(where: $where) {
name
}
}`),
);

getVariablesJSONSchema(variableToType, { useMarkdownDescription: true });
expect(true).toEqual(true);
});
});
142 changes: 83 additions & 59 deletions packages/graphql-language-service/src/utils/getVariablesJSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ export type JSONSchemaOptions = {
*/
useMarkdownDescription?: boolean;
};
type JSONSchemaRunningOptions = JSONSchemaOptions & {
definitionMarker: Marker;
};

export const defaultJSONSchemaOptions = {
useMarkdownDescription: false,
Expand Down Expand Up @@ -106,14 +109,27 @@ const scalarTypesMap: { [key: string]: JSONSchema6TypeName } = {
DateTime: 'string',
};

class Marker {
private set = new Set<string>();
mark(name: string): boolean {
if (this.set.has(name)) {
return false;
} else {
this.set.add(name);
return true;
}
}
}

/**
*
* @param type {GraphQLInputType}
* @param options
* @returns {DefinitionResult}
*/
function getJSONSchemaFromGraphQLType(
type: GraphQLInputType | GraphQLInputField,
options?: JSONSchemaOptions,
options?: JSONSchemaRunningOptions,
): DefinitionResult {
let required = false;
let definition: CombinedSchema = Object.create(null);
Expand Down Expand Up @@ -164,70 +180,73 @@ function getJSONSchemaFromGraphQLType(
}
if (isInputObjectType(type)) {
definition.$ref = `#/definitions/${type.name}`;
const fields = type.getFields();
if (options?.definitionMarker.mark(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);
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);
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);
const {
definition: fieldDefinition,
// definitions: fieldDefinitions,
} = getJSONSchemaFromGraphQLType(field, options);

fieldDef.properties[fieldName] = {
...typeDefinition,
...fieldDefinition,
} as JSONSchema6;
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;
}
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;
if (fieldRequired) {
fieldDef.required!.push(fieldName);
}
if (typeDefinitions) {
Object.keys(typeDefinitions).map(defName => {
definitions[defName] = typeDefinitions[defName];
});
}
});
definitions![type.name] = fieldDef;
}
}
// append descriptions
if (
Expand Down Expand Up @@ -300,11 +319,16 @@ export function getVariablesJSONSchema(
required: [],
};

const runtimeOptions: JSONSchemaRunningOptions = {
...options,
definitionMarker: new Marker(),
};

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);
getJSONSchemaFromGraphQLType(type, runtimeOptions);
jsonSchema.properties[variableName] = definition;
if (required) {
jsonSchema.required?.push(variableName);
Expand Down

0 comments on commit f788e65

Please sign in to comment.