graphql-codegen-typescript-validation-schema
Version:
GraphQL Code Generator plugin to generate form validation schema from your GraphQL schema
181 lines (180 loc) • 7.63 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.isInput = exports.isNamedType = exports.isNonNullType = exports.isListType = void 0;
exports.ObjectTypeDefinitionBuilder = ObjectTypeDefinitionBuilder;
exports.InterfaceTypeDefinitionBuilder = InterfaceTypeDefinitionBuilder;
exports.topologicalSortAST = topologicalSortAST;
exports.topsort = topsort;
exports.isGeneratedByIntrospection = isGeneratedByIntrospection;
exports.escapeGraphQLCharacters = escapeGraphQLCharacters;
const graphlib_1 = require("graphlib");
const graphql_1 = require("graphql");
/**
* Recursively unwraps a GraphQL type until it reaches the NamedType.
*
* Since a GraphQL type is defined as either a NamedTypeNode, ListTypeNode, or NonNullTypeNode,
* this implementation safely recurses until the underlying NamedTypeNode is reached.
*/
function getNamedType(typeNode) {
return typeNode.kind === graphql_1.Kind.NAMED_TYPE ? typeNode : getNamedType(typeNode.type);
}
const isListType = (typ) => typ?.kind === graphql_1.Kind.LIST_TYPE;
exports.isListType = isListType;
const isNonNullType = (typ) => typ?.kind === graphql_1.Kind.NON_NULL_TYPE;
exports.isNonNullType = isNonNullType;
const isNamedType = (typ) => typ?.kind === graphql_1.Kind.NAMED_TYPE;
exports.isNamedType = isNamedType;
const isInput = (kind) => kind.includes('Input');
exports.isInput = isInput;
function ObjectTypeDefinitionBuilder(useObjectTypes, callback) {
if (!useObjectTypes)
return undefined;
return (node) => {
if (/^(Query|Mutation|Subscription)$/.test(node.name.value))
return;
return callback(node);
};
}
function InterfaceTypeDefinitionBuilder(useInterfaceTypes, callback) {
if (!useInterfaceTypes)
return undefined;
return (node) => {
return callback(node);
};
}
function topologicalSortAST(schema, ast) {
const dependencyGraph = new graphlib_1.Graph();
const targetKinds = [
graphql_1.Kind.OBJECT_TYPE_DEFINITION,
graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION,
graphql_1.Kind.INTERFACE_TYPE_DEFINITION,
graphql_1.Kind.SCALAR_TYPE_DEFINITION,
graphql_1.Kind.ENUM_TYPE_DEFINITION,
graphql_1.Kind.UNION_TYPE_DEFINITION,
];
(0, graphql_1.visit)(ast, {
enter: (node) => {
switch (node.kind) {
case graphql_1.Kind.OBJECT_TYPE_DEFINITION:
case graphql_1.Kind.INPUT_OBJECT_TYPE_DEFINITION:
case graphql_1.Kind.INTERFACE_TYPE_DEFINITION: {
const typeName = node.name.value;
dependencyGraph.setNode(typeName);
if (node.fields) {
node.fields.forEach((field) => {
// Unwrap the type
const namedTypeNode = getNamedType(field.type);
const dependency = namedTypeNode.name.value;
const namedType = schema.getType(dependency);
if (namedType?.astNode?.kind === undefined
|| !targetKinds.includes(namedType.astNode.kind)) {
return;
}
if (!dependencyGraph.hasNode(dependency)) {
dependencyGraph.setNode(dependency);
}
dependencyGraph.setEdge(typeName, dependency);
});
}
break;
}
case graphql_1.Kind.SCALAR_TYPE_DEFINITION:
case graphql_1.Kind.ENUM_TYPE_DEFINITION: {
dependencyGraph.setNode(node.name.value);
break;
}
case graphql_1.Kind.UNION_TYPE_DEFINITION: {
const dependency = node.name.value;
if (!dependencyGraph.hasNode(dependency))
dependencyGraph.setNode(dependency);
node.types?.forEach((type) => {
const dependency = type.name.value;
const typ = schema.getType(dependency);
if (typ?.astNode?.kind === undefined || !targetKinds.includes(typ.astNode.kind))
return;
dependencyGraph.setEdge(node.name.value, dependency);
});
break;
}
default:
break;
}
},
});
const sorted = topsort(dependencyGraph);
// Create a map of definitions for quick access, using the definition's name as the key.
const definitionsMap = new Map();
// SCHEMA_DEFINITION does not have a name.
// https://spec.graphql.org/October2021/#sec-Schema
const astDefinitions = ast.definitions.filter(def => def.kind !== graphql_1.Kind.SCHEMA_DEFINITION);
astDefinitions.forEach((definition) => {
if (hasNameField(definition) && definition.name)
definitionsMap.set(definition.name.value, definition);
});
// Two arrays to store sorted and unsorted definitions.
const sortedDefinitions = [];
const notSortedDefinitions = [];
// Iterate over sorted type names and retrieve their corresponding definitions.
sorted.forEach((sortedType) => {
const definition = definitionsMap.get(sortedType);
if (definition) {
sortedDefinitions.push(definition);
definitionsMap.delete(sortedType);
}
});
// Definitions that are left in the map were not included in sorted list
// Add them to notSortedDefinitions.
definitionsMap.forEach(definition => notSortedDefinitions.push(definition));
const newDefinitions = [...sortedDefinitions, ...notSortedDefinitions];
if (newDefinitions.length !== astDefinitions.length) {
throw new Error(`Unexpected definition length after sorting: want ${astDefinitions.length} but got ${newDefinitions.length}`);
}
return {
...ast,
definitions: newDefinitions,
};
}
function hasNameField(node) {
return 'name' in node;
}
/**
* [Re-implemented topsort function][topsort-ref] with cycle handling. This version iterates over
* all nodes in the graph to ensure every node is visited, even if the graph contains cycles.
*
* [topsort-ref]: https://github.com/dagrejs/graphlib/blob/8d27cb89029081c72eb89dde652602805bdd0a34/lib/alg/topsort.js
*/
function topsort(g) {
const visited = new Set();
const results = [];
function visit(node) {
if (visited.has(node))
return;
visited.add(node);
// Recursively visit all predecessors of the node.
g.predecessors(node)?.forEach(visit);
results.push(node);
}
// Visit every node in the graph (instead of only sinks)
g.nodes().forEach(visit);
return results.reverse();
}
function isGeneratedByIntrospection(schema) {
return Object.entries(schema.getTypeMap())
.filter(([name, type]) => !name.startsWith('__') && !(0, graphql_1.isSpecifiedScalarType)(type))
.every(([, type]) => type.astNode === undefined);
}
// https://spec.graphql.org/October2021/#EscapedCharacter
const escapeMap = {
'\"': '\\\"',
'\\': '\\\\',
'\/': '\\/',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
};
function escapeGraphQLCharacters(input) {
// eslint-disable-next-line regexp/no-escape-backspace
return input.replace(/["\\/\f\n\r\t\b]/g, match => escapeMap[match]);
}