graphql-mandatory-validator
Version:
A GraphQL schema validator using AST-only parsing for mandatory fields with default values, array validation, and composite type validation
751 lines • 37.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.GraphQLValidator = void 0;
const child_process_1 = require("child_process");
const path = __importStar(require("path"));
const fs = __importStar(require("fs"));
const graphql_1 = require("graphql");
const parser_1 = require("graphql/language/parser");
/**
* Parse GraphQL file contents using Parser class
*/
function parseGraphqlFile(graphqlFileContents) {
const parser = new parser_1.Parser(graphqlFileContents);
return parser.parseDocument();
}
class GraphQLValidator {
constructor(options = {}) {
this.options = {
baseDir: options.baseDir || 'src/type-defs',
scalarDefaults: options.scalarDefaults || {
String: '@defaultValue(value: "")',
Int: '@defaultValue(value: 0)',
Float: '@defaultValue(value: 0.0)',
Boolean: '@defaultValue(value: false)',
ID: '@defaultValue(value: "")',
},
colorOutput: options.colorOutput !== false,
exitOnError: options.exitOnError !== false,
};
}
/**
* Get staged .graphql files from git
*/
getStagedGraphqlFiles() {
try {
const output = (0, child_process_1.execSync)('git diff --cached --name-only', { encoding: 'utf-8' });
return output
.split('\n')
.filter((file) => file.trim() && file.endsWith('.graphql'))
.filter((file) => file.includes(this.options.baseDir))
.map((file) => path.resolve(file));
}
catch (error) {
console.warn('Warning: Could not get staged files from git. Not in a git repository?');
return [];
}
}
/**
* Get all .graphql files in the base directory
*/
getAllGraphqlFiles(projectRoot = process.cwd()) {
try {
const searchDir = path.join(projectRoot, this.options.baseDir);
const output = (0, child_process_1.execSync)(`find "${searchDir}" -name "*.graphql" -type f`, { encoding: 'utf-8' });
return output
.split('\n')
.filter((file) => file.trim())
.map((file) => path.resolve(file));
}
catch (error) {
console.warn(`Warning: Could not find GraphQL files in ${this.options.baseDir}`);
return [];
}
}
/**
* Validate newly added mandatory fields in staged files
*/
async validateStagedFiles() {
const graphqlFiles = this.getStagedGraphqlFiles();
if (graphqlFiles.length === 0) {
console.log('No staged .graphql files to check.');
return { success: true, errors: [], filesChecked: 0 };
}
return this.validateFiles(graphqlFiles, true);
}
/**
* Validate all GraphQL files in the project
*/
async validateProject(projectRoot = process.cwd()) {
const graphqlFiles = this.getAllGraphqlFiles(projectRoot);
if (graphqlFiles.length === 0) {
console.log(`No .graphql files found in ${this.options.baseDir}`);
return { success: true, errors: [], filesChecked: 0 };
}
return this.validateFiles(graphqlFiles, false);
}
/**
* Parse git diff to extract added lines with correct line numbers
*/
parseGitDiffForAddedLines(diff) {
const lines = diff.split('\n');
const addedLines = [];
let currentLineNumber = 0;
for (const line of lines) {
if (line.startsWith('@@')) {
// Parse hunk header to get starting line number
// Format: @@ -oldStart,oldCount +newStart,newCount @@
const parts = line.split(' ');
if (parts.length >= 3 && parts[2].startsWith('+')) {
const newStartPart = parts[2].substring(1); // Remove '+' prefix
const newStart = newStartPart.includes(',')
? parseInt(newStartPart.split(',')[0], 10)
: parseInt(newStartPart, 10);
if (!isNaN(newStart)) {
currentLineNumber = newStart - 1; // -1 because we increment before processing
}
}
}
else if (line.startsWith('+') && !line.startsWith('+++')) {
// This is an added line
currentLineNumber++;
addedLines.push({
content: line.slice(1), // Remove the '+' prefix
lineNumber: currentLineNumber
});
}
else if (!line.startsWith('-') && !line.startsWith('\\')) {
// This is a context line (unchanged), increment line number
currentLineNumber++;
}
// For deleted lines (start with '-'), we don't increment currentLineNumber
}
return addedLines;
}
/**
* Parse GraphQL schema from files to understand type definitions
* For enum validation, we need to parse ALL files in the project to get complete enum registry
*/
parseGraphQLSchema(files) {
const schemaContext = {
types: new Map(),
enums: new Map(),
mandatoryCompositeFields: []
};
// Get all GraphQL files in the project for complete enum registry
const allGraphQLFiles = this.getAllGraphqlFiles();
// First pass: Parse ALL files to build complete enum registry
allGraphQLFiles.forEach(file => {
try {
const content = fs.readFileSync(file, 'utf-8');
this.parseEnumsFromFile(content, file, schemaContext);
}
catch (error) {
console.warn(`Warning: Could not parse enums from ${file}:`, error);
}
});
// Second pass: Parse only the files being validated for types and fields
files.forEach(file => {
try {
const content = fs.readFileSync(file, 'utf-8');
this.parseFileSchema(content, file, schemaContext);
}
catch (error) {
console.warn(`Warning: Could not parse schema from ${file}:`, error);
}
});
return schemaContext;
}
/**
* Parse enums from a single GraphQL file to build complete enum registry
*/
parseEnumsFromFile(content, file, schemaContext) {
try {
const document = parseGraphqlFile(content);
// Only collect enum definitions
document.definitions.forEach(definition => {
if (definition.kind === graphql_1.Kind.ENUM_TYPE_DEFINITION) {
const enumDef = definition;
const enumName = enumDef.name.value;
const enumValues = enumDef.values?.map(v => v.name.value) || [];
schemaContext.enums.set(enumName, enumValues);
}
});
}
catch (error) {
// Silently skip files that can't be parsed for enums
// This ensures we don't break validation for files with syntax errors
}
}
/**
* Parse a single GraphQL file using GraphQL AST parser
*/
parseFileSchema(content, file, schemaContext) {
try {
// Parse type definitions (enums are already parsed in first pass)
const document = parseGraphqlFile(content);
const lines = content.split('\n');
// Process type definitions (both regular types and extended types)
document.definitions.forEach(definition => {
if (definition.kind === graphql_1.Kind.OBJECT_TYPE_DEFINITION || definition.kind === graphql_1.Kind.OBJECT_TYPE_EXTENSION) {
const typeDef = definition;
const typeName = typeDef.name.value;
const fields = [];
if (typeDef.fields) {
typeDef.fields.forEach(fieldDef => {
const fieldName = fieldDef.name.value;
const fieldType = this.extractTypeFromAST(fieldDef.type);
const isMandatory = fieldDef.type.kind === graphql_1.Kind.NON_NULL_TYPE;
// Check for @defaultValue directive - handle both inline and on next line
let hasDefaultValue = false;
let defaultValue;
// Check for directive on the field definition
if (fieldDef.directives) {
const defaultDirective = fieldDef.directives.find(directive => directive.name.value === 'defaultValue');
if (defaultDirective) {
hasDefaultValue = true;
const valueArg = defaultDirective.arguments?.find(arg => arg.name.value === 'value');
if (valueArg) {
if (valueArg.value.kind === graphql_1.Kind.STRING) {
defaultValue = valueArg.value.value;
}
else if (valueArg.value.kind === graphql_1.Kind.LIST) {
// Handle array default values like @defaultValue(value: [])
defaultValue = '[]';
}
else if (valueArg.value.kind === graphql_1.Kind.ENUM) {
// Handle unquoted enum values like @defaultValue(value: True)
defaultValue = valueArg.value.value;
}
}
}
}
// We now rely solely on AST parsing for directives
// No regex-based fallback for directive detection
const fieldLine = this.getLineNumberFromLocation(fieldDef.loc, lines);
const field = {
name: fieldName,
type: fieldType,
isMandatory,
hasDefaultValue,
defaultValue,
line: fieldLine,
astNode: fieldDef
};
fields.push(field);
// Track mandatory composite fields (non-scalar types)
if (isMandatory && !this.options.scalarDefaults[fieldType]) {
schemaContext.mandatoryCompositeFields.push({
fieldName,
fieldType,
file,
line: fieldLine
});
}
});
}
const typeStartLine = this.getLineNumberFromLocation(typeDef.loc, lines);
// For extend types, merge with existing type if it exists, otherwise create new
const existingType = schemaContext.types.get(typeName);
if (existingType && definition.kind === graphql_1.Kind.OBJECT_TYPE_EXTENSION) {
// Merge fields from extension into existing type
existingType.fields.push(...fields);
// Update mandatory composite fields tracking
fields.forEach(field => {
if (field.isMandatory && !this.options.scalarDefaults[field.type]) {
schemaContext.mandatoryCompositeFields.push({
fieldName: field.name,
fieldType: field.type,
file,
line: field.line
});
}
});
}
else {
// Create new type entry
const graphqlType = {
name: typeName,
fields,
file,
startLine: typeStartLine,
astNode: typeDef
};
schemaContext.types.set(typeName, graphqlType);
}
}
});
}
catch (error) {
console.warn(`Warning: Could not parse GraphQL file ${file}:`, error);
// Fallback to the old regex-based parsing if AST parsing fails
this.parseFileSchemeFallback(content, file, schemaContext);
}
}
/**
* Extract type name from GraphQL AST type node
*/
extractTypeFromAST(typeNode) {
if (typeNode.kind === graphql_1.Kind.NON_NULL_TYPE) {
return this.extractTypeFromAST(typeNode.type);
}
if (typeNode.kind === graphql_1.Kind.LIST_TYPE) {
return `[${this.extractTypeFromAST(typeNode.type)}]`;
}
if (typeNode.kind === graphql_1.Kind.NAMED_TYPE) {
return typeNode.name.value;
}
return 'Unknown';
}
/**
* Get line number from GraphQL AST location
*/
getLineNumberFromLocation(loc, lines) {
if (!loc)
return 1;
return loc.startToken.line;
}
/**
* Check if a type is an array/list type based on AST parsing
*/
isArrayType(type) {
return type.startsWith('[') && type.endsWith(']');
}
/**
* Check if a string is a valid GraphQL identifier without using regex
*/
isValidIdentifier(identifier) {
if (!identifier || identifier.length === 0)
return false;
// First character must be letter or underscore
const firstChar = identifier.charAt(0);
if (!((firstChar >= 'A' && firstChar <= 'Z') ||
(firstChar >= 'a' && firstChar <= 'z') ||
firstChar === '_')) {
return false;
}
// Remaining characters must be letters, digits, or underscores
for (let i = 1; i < identifier.length; i++) {
const char = identifier.charAt(i);
if (!((char >= 'A' && char <= 'Z') ||
(char >= 'a' && char <= 'z') ||
(char >= '0' && char <= '9') ||
char === '_')) {
return false;
}
}
return true;
}
/**
* Fallback AST-based parsing for when primary AST parsing fails
* This is a simplified version that only extracts basic type information
*/
parseFileSchemeFallback(content, file, schemaContext) {
try {
// Try to parse with AST again but with more lenient error handling
const document = parseGraphqlFile(content);
const lines = content.split('\n');
// Enums are already parsed in the first pass, skip enum processing here
// Process type definitions (both regular types and extended types)
document.definitions.forEach(definition => {
if (definition.kind === graphql_1.Kind.OBJECT_TYPE_DEFINITION || definition.kind === graphql_1.Kind.OBJECT_TYPE_EXTENSION) {
const typeDef = definition;
const typeName = typeDef.name.value;
const fields = [];
if (typeDef.fields) {
typeDef.fields.forEach(fieldDef => {
const fieldName = fieldDef.name.value;
const fieldType = this.extractTypeFromAST(fieldDef.type);
const isMandatory = fieldDef.type.kind === graphql_1.Kind.NON_NULL_TYPE;
// Check for @defaultValue directive - AST only
let hasDefaultValue = false;
let defaultValue;
if (fieldDef.directives) {
const defaultDirective = fieldDef.directives.find(d => d.name.value === 'defaultValue');
if (defaultDirective) {
hasDefaultValue = true;
const valueArg = defaultDirective.arguments?.find(arg => arg.name.value === 'value');
if (valueArg) {
if (valueArg.value.kind === graphql_1.Kind.STRING) {
defaultValue = valueArg.value.value;
}
else if (valueArg.value.kind === graphql_1.Kind.LIST) {
defaultValue = '[]';
}
}
}
}
const fieldLine = this.getLineNumberFromLocation(fieldDef.loc, lines);
fields.push({
name: fieldName,
type: fieldType,
isMandatory,
hasDefaultValue,
defaultValue,
line: fieldLine,
astNode: fieldDef
});
// Track mandatory composite fields (non-scalar types)
if (isMandatory && !this.options.scalarDefaults[fieldType]) {
schemaContext.mandatoryCompositeFields.push({
fieldName,
fieldType,
file,
line: fieldLine
});
}
});
}
const typeStartLine = this.getLineNumberFromLocation(typeDef.loc, lines);
// For extend types, merge with existing type if it exists, otherwise create new
const existingType = schemaContext.types.get(typeName);
if (existingType && definition.kind === graphql_1.Kind.OBJECT_TYPE_EXTENSION) {
// Merge fields from extension into existing type
existingType.fields.push(...fields);
}
else {
// Create new type entry
schemaContext.types.set(typeName, {
name: typeName,
fields,
file,
startLine: typeStartLine,
astNode: typeDef
});
}
}
});
}
catch (error) {
console.warn(`Warning: Complete fallback parsing failed for ${file}:`, error);
// If even the fallback AST parsing fails, we'll have an empty schema context for this file
}
}
/**
* Validate composite fields using AST-based approach with line filtering
*/
validateCompositeFields(schemaContext, linesToValidate, onlyDiffChanges) {
const errors = [];
schemaContext.types.forEach((graphqlType) => {
graphqlType.fields.forEach((field) => {
const fieldKey = `${graphqlType.file}:${field.line}`;
// Skip this field if we're in staged mode and this line wasn't added
if (onlyDiffChanges && !linesToValidate.has(fieldKey)) {
return;
}
if (field.isMandatory) {
// Check if this is a composite type (not a scalar, not a list)
const isScalar = !!this.options.scalarDefaults[field.type];
const isListType = field.type.startsWith('[') && field.type.endsWith(']');
const isCompositeType = schemaContext.types.has(field.type);
if (!isScalar && !isListType && isCompositeType) {
const compositeType = schemaContext.types.get(field.type);
// Only validate if the composite type is defined in the same file
if (compositeType && compositeType.file === graphqlType.file) {
const hasMandatoryFields = compositeType.fields.some(f => f.isMandatory);
if (!hasMandatoryFields) {
errors.push({
file: graphqlType.file,
line: field.line,
fieldName: field.name,
fieldType: field.type,
message: `Mandatory composite field "${field.name}" of type "${field.type}!" references a type with no mandatory fields. Type "${field.type}" should have at least one mandatory field.`,
errorType: 'empty-composite'
});
}
}
// If composite type is in a different file, skip validation (let it pass)
}
}
});
});
return errors;
}
/**
* Validate that mandatory composite types have at least one mandatory field
* Only checks newly added fields when onlyDiffChanges is true
*/
validateCompositeTypes(schemaContext, files, onlyDiffChanges) {
const errors = [];
if (onlyDiffChanges) {
// Only check newly added mandatory composite fields
files.forEach(file => {
try {
const diff = (0, child_process_1.execSync)(`git diff --cached "${file}"`, { encoding: 'utf-8' });
const addedLines = this.parseGitDiffForAddedLines(diff);
addedLines.forEach(({ content, lineNumber }) => {
// Check if this line contains a newly added mandatory composite field
const trimmedContent = content.trim();
// Parse field definition: fieldName: FieldType!
if (trimmedContent.includes(':') && trimmedContent.includes('!')) {
const colonIndex = trimmedContent.indexOf(':');
const exclamationIndex = trimmedContent.indexOf('!');
if (colonIndex > 0 && exclamationIndex > colonIndex) {
const fieldName = trimmedContent.substring(0, colonIndex).trim();
const typeSection = trimmedContent.substring(colonIndex + 1, exclamationIndex).trim();
const fieldType = typeSection;
// Simple validation: field name should be valid identifier, type should not be empty
if (fieldName && fieldType && this.isValidIdentifier(fieldName) && fieldType.length > 0) {
// Check if this is a composite type (not a scalar)
if (!this.options.scalarDefaults[fieldType]) {
const compositeType = schemaContext.types.get(fieldType);
if (compositeType) {
const hasMandatoryFields = compositeType.fields.some(field => field.isMandatory);
if (!hasMandatoryFields) {
errors.push({
file,
line: lineNumber,
fieldName,
fieldType,
message: `Mandatory composite field "${fieldName}" of type "${fieldType}!" references a type with no mandatory fields. Type "${fieldType}" should have at least one mandatory field.`,
errorType: 'empty-composite'
});
}
}
}
}
}
}
});
}
catch (error) {
console.error(`Failed to get diff for file ${file}:`, error);
}
});
}
else {
// Check all mandatory composite fields (full validation mode)
schemaContext.mandatoryCompositeFields.forEach(mandatoryField => {
const compositeType = schemaContext.types.get(mandatoryField.fieldType);
if (compositeType) {
const hasMandatoryFields = compositeType.fields.some(field => field.isMandatory);
if (!hasMandatoryFields) {
errors.push({
file: mandatoryField.file,
line: mandatoryField.line,
fieldName: mandatoryField.fieldName,
fieldType: mandatoryField.fieldType,
message: `Mandatory composite field "${mandatoryField.fieldName}" of type "${mandatoryField.fieldType}!" references a type with no mandatory fields. Type "${mandatoryField.fieldType}" should have at least one mandatory field.`,
errorType: 'empty-composite'
});
}
}
});
}
return errors;
}
/**
* Validate specific GraphQL files using AST-based approach
*/
async validateFiles(files, onlyDiffChanges) {
const errors = [];
// Parse schema to understand type definitions
const schemaContext = this.parseGraphQLSchema(files);
// Get lines that should be validated (all lines or only added lines)
const linesToValidate = new Set();
if (onlyDiffChanges) {
// For staged mode, collect all line numbers that were added
files.forEach(file => {
try {
const diff = (0, child_process_1.execSync)(`git diff --cached "${file}"`, { encoding: 'utf-8' });
const addedLines = this.parseGitDiffForAddedLines(diff);
addedLines.forEach(({ lineNumber }) => {
linesToValidate.add(`${file}:${lineNumber}`);
});
}
catch (error) {
console.warn(`Warning: Could not get git diff for ${file}:`, error);
}
});
}
// Validate composite fields using AST-based approach
const compositeErrors = this.validateCompositeFields(schemaContext, linesToValidate, onlyDiffChanges);
errors.push(...compositeErrors);
// Validate all types and fields using AST data
schemaContext.types.forEach((graphqlType) => {
graphqlType.fields.forEach((field) => {
const fieldKey = `${graphqlType.file}:${field.line}`;
// Skip this field if we're in staged mode and this line wasn't added
if (onlyDiffChanges && !linesToValidate.has(fieldKey)) {
return;
}
if (field.isMandatory) {
const expectedScalarDefault = this.options.scalarDefaults[field.type];
if (expectedScalarDefault) {
// Handle scalar types
if (!field.hasDefaultValue) {
const error = {
file: graphqlType.file,
line: field.line,
fieldName: field.name,
fieldType: field.type,
expectedDefault: expectedScalarDefault,
message: `Mandatory field "${field.name}" of type "${field.type}!" must have a default value ${expectedScalarDefault}`,
errorType: 'missing-default'
};
errors.push(error);
}
}
else {
// Check if it's an array/list type
const isArrayType = this.isArrayType(field.type);
if (isArrayType) {
// Array types require @defaultValue(value: []) for mandatory fields
if (!field.hasDefaultValue) {
errors.push({
file: graphqlType.file,
line: field.line,
fieldName: field.name,
fieldType: field.type,
expectedDefault: '@defaultValue(value: [])',
message: `Mandatory array field "${field.name}" of type "${field.type}!" must have a default value @defaultValue(value: [])`,
errorType: 'missing-array-default'
});
}
else if (field.defaultValue !== '[]') {
errors.push({
file: graphqlType.file,
line: field.line,
fieldName: field.name,
fieldType: field.type,
expectedDefault: '@defaultValue(value: [])',
message: `Mandatory array field "${field.name}" of type "${field.type}!" must have default value [] but got "${field.defaultValue}"`,
errorType: 'invalid-array-default'
});
}
return;
}
// Check if it's a composite type or enum (defined in schema context)
const isCompositeType = schemaContext.types.has(field.type);
const isEnumType = schemaContext.enums.has(field.type);
// For enums, check if the default value is one of the enum values
if (isEnumType) {
const enumValues = schemaContext.enums.get(field.type) || [];
if (field.hasDefaultValue && field.defaultValue && !enumValues.includes(field.defaultValue)) {
errors.push({
file: graphqlType.file,
line: field.line,
fieldName: field.name,
fieldType: field.type,
message: `Invalid default value "${field.defaultValue}" for enum field "${field.name}" of type "${field.type}!". Must be one of: ${enumValues.join(', ')}`,
errorType: 'invalid-enum-default'
});
}
else if (!field.hasDefaultValue) {
errors.push({
file: graphqlType.file,
line: field.line,
fieldName: field.name,
fieldType: field.type,
message: `Mandatory enum field "${field.name}" of type "${field.type}!" must have a default value. Must be one of: ${enumValues.join(', ')}`,
errorType: 'missing-enum-default'
});
}
}
// For composite types, skip here as they are handled by validateCompositeFields
// to avoid duplicate validation
// For unknown types (not scalar, not enum, not composite), only validate if no default value
// This handles cases where types might be defined in other files or are built-in types
else if (!field.hasDefaultValue) {
// Check if this looks like a custom type (starts with uppercase) vs built-in type
const isCustomType = field.type.length > 0 && field.type.charAt(0) >= 'A' && field.type.charAt(0) <= 'Z';
if (isCustomType) {
// This is likely a composite type defined in another file - skip validation
// to avoid false positives for cross-file dependencies
return;
}
else {
// This might be an unknown scalar or enum type
const error = {
file: graphqlType.file,
line: field.line,
fieldName: field.name,
fieldType: field.type,
message: `Mandatory field "${field.name}" of type "${field.type}!" must have a default value.`,
errorType: 'missing-enum-default'
};
errors.push(error);
}
}
}
}
});
});
const result = {
success: errors.length === 0,
errors,
filesChecked: files.length,
};
this.reportResults(result);
return result;
}
/**
* Report validation results
*/
reportResults(result) {
if (result.success) {
const color = this.options.colorOutput ? '\x1b[32;1m' : '';
const reset = this.options.colorOutput ? '\x1b[0m' : '';
console.log(`${color}✓ GraphQL validation passed for ${result.filesChecked} files${reset}`);
}
else {
const errorColor = this.options.colorOutput ? '\x1b[31;1m' : '';
const reset = this.options.colorOutput ? '\x1b[0m' : '';
console.error(`${errorColor}✗ GraphQL validation failed with ${result.errors.length} errors:${reset}`);
result.errors.forEach((error) => {
console.error(`${errorColor}Error in ${error.file} at line ${error.line}: ${error.message}${reset}`);
});
if (this.options.exitOnError) {
process.exit(1);
}
}
}
/**
* Run validation (convenience method for CLI usage)
*/
async run(mode = 'staged', projectRoot) {
if (mode === 'staged') {
return this.validateStagedFiles();
}
else {
return this.validateProject(projectRoot);
}
}
}
exports.GraphQLValidator = GraphQLValidator;
// Default export for convenience
exports.default = GraphQLValidator;
//# sourceMappingURL=index.js.map