UNPKG

graphql-query-gen

Version:

Node.js module to generate queries with random input data from graphQL endpoint or schema

405 lines (361 loc) 16 kB
const { getIntrospectionQuery, buildClientSchema, buildSchema, introspectionFromSchema, printSchema } = require('graphql'); const fetch = require('node-fetch'); const { v4: uuidv4 } = require('uuid'); var stringSimilarity = require("string-similarity"); class HTTPResponseError extends Error { constructor(response, ...args) { super(`HTTP Error Response: ${response.status} ${response.statusText}`, ...args); this.response = response; } } exports.processEndpoint = async function (url, options) { //Prepare to fetch sdl const requestHeaders = Object.assign({"Content-Type": "application/json"}, options.headers|| {}); var data = { "query": getIntrospectionQuery() }; var requestOptions = { method: 'POST', headers: requestHeaders, body: JSON.stringify(data), redirect: 'follow' }; //Fetch SDL const response = await fetch(url, requestOptions) if (!response || !response.ok) { throw new HTTPResponseError(response); } const json = await response.json(); if(json['errors'] && json.errors[0]) { if(json.errors[0]['extensions']) throw new Error(`${json.errors[0].extensions.code} - ${json.errors[0].message}`); else throw new Error(`${json.errors[0].message}`); } const schema = printSchema(buildClientSchema(json.data, { assumeValid: true })) return exports.process(json.data, schema, options); } exports.processSchema = function (schema, options) { const graphqlSchema = buildSchema(schema); const introspection = introspectionFromSchema(graphqlSchema); return exports.process(introspection, schema, options); } exports.processSDL = function (sdl, options) { const schema = printSchema(buildClientSchema(sdl, { assumeValid: true })) return exports.process(sdl, schema, options); } // TODO: Process fragments, subscriptions exports.process = function (sdl, schema, options) { console.log('====================STARTING====================') // Merge user options with defaults const defaults = { debug: false, filter: null, responseDepth: 5, inputDepth: 7, spacer: ' ', indentBy: 4, inputVariables: false, duplicatePercentage: 75, operationName: true, comments: true } options = Object.assign({}, defaults, options); if (!options.debug) console.debug = function () { }; //Start processingL var queries = listOperations(sdl.__schema.types, 'Query', options.filter, options.inputDepth, options.responseDepth, options.spacer, options.indentBy, options.inputVariables, options.operationName, options.comments); var mutations = listOperations(sdl.__schema.types, 'Mutation', options.filter, options.inputDepth, options.responseDepth, options.spacer, options.indentBy, options.inputVariables, options.operationName, options.comments); var subscriptions = []; var types = listTypes(sdl.__schema.types, 'OBJECT', options.spacer, options.indentBy); var inputs = listInputs(sdl.__schema.types, 'INPUT_OBJECT', options.spacer, options.indentBy); var duplicates = listDuplicates(types, inputs, options.duplicatePercentage); console.log('====================FINISHED====================') return { operations: [ { name: 'Query', options: queries }, { name: 'Mutation', options: mutations }, { name: 'Subscription', options: subscriptions } ], types: types, inputs: inputs, statistics: { counts: { queries: queries.length, mutations: mutations.length, subscriptions: subscriptions.length, types: types.length, inputs: inputs.length }, suggestions: { duplicates: duplicates } }, schema: schema }; } function listDuplicates(types, inputs, duplicatePercentage) { var duplicates = findDuplicates(types, 'Type', duplicatePercentage); duplicates = duplicates.concat(findDuplicates(inputs, 'Input', duplicatePercentage)); return duplicates; } function findDuplicates(list, prefix, duplicatePercentage) { const duplicates = []; for (var i = 0; i < list.length; i++) { for (var j = i + 1; j < list.length; j++) { const similarity = stringSimilarity.compareTwoStrings(list[i].definition, list[j].definition); const similarityPercentage = (similarity * 100).toFixed(2); if (similarityPercentage >= duplicatePercentage) duplicates.push(`${prefix} ${list[i].name} is ${similarityPercentage}% similar to ${list[j].name}`); } } return duplicates; } function listTypes(types, kind, spacer, indent) { const excludeList = ['Query', 'Mutation', 'Subscription']; const resultTypes = types.filter(type => type.kind == kind && !type.name.startsWith('__') && excludeList.indexOf(type.name) == -1); const results = []; resultTypes.forEach(t => { const fields = []; t.fields.forEach(f => { fields.push(`${f.name}: ${generateTypeValue(f.type)}`); }); results.push({ name: t.name, definition: `type ${t.name} {\n${spacer.repeat(indent)}${fields.join('\n' + spacer.repeat(indent))}\n}` }); }); return results; } function listInputs(types, kind, spacer, indent) { const inputTypes = types.filter(type => type.kind == kind); const inputs = []; inputTypes.forEach(t => { const inputFields = []; t.inputFields.forEach(f => { inputFields.push(`${f.name}: ${generateTypeValue(f.type)}`); }); inputs.push({ name: t.name, definition: `input ${t.name} {\n${spacer.repeat(indent)}${inputFields.join('\n' + spacer.repeat(indent))}\n}` }); }); return inputs; } function listOperations(types, typeName, filter, inputDepth, responseDepth, spacer, indentBy, inputVariables, operationName, comments) { console.log(`\nPreparing for operation type: ${typeName}`); var ops = []; const opType = types.filter(type => type.name == typeName)[0]; if (opType) { if (filter) console.debug(` - Filtering fields as per filter: ${filter}`); const filteredFields = opType.fields.filter(field => filter ? field.name.includes(filter) : field); if (opType.fields.length == 0) console.debug(` - No fields found`); else if (filteredFields.length == 0) console.debug(` - No matching fields found as per your current filter`); else filteredFields.forEach(field => { const op = generateOperation(types, typeName, field.name, field.description, field.args, field.type, inputDepth, responseDepth, spacer, indentBy, inputVariables, operationName, comments); ops.push(op); }); } else console.debug(` - Operation type not found`); return ops; } function generateOperation(types, typeName, opName, opDescription, args, resType, inputDepth, responseDepth, spacer, indentBy, inputVariables, operationName, comments) { console.debug(`${' '.repeat(indentBy)}- generateOperation ${opName}`); const argsInput = generateArgsInput(types, args, indentBy * 2, inputDepth, spacer, indentBy, inputVariables, comments); const resFields = generateResponseFields(types, resType, indentBy * 2, responseDepth, spacer, indentBy); const printedOperationName = operationName? ` ${opName}`: ''; const printedComments = (comments && opDescription)? `# ${opDescription}\n`: ''; var resDef = ''; if (resFields) resDef = ` { ${spacer.repeat(indentBy * 2)}__typename ${resFields} ${spacer.repeat(indentBy)}}`; const query = `${printedComments}${typeName.toLowerCase()}${printedOperationName}${argsInput.typeVars} { ${spacer.repeat(indentBy)}${opName} ${argsInput.input}${resDef} }`; return { name: opName, query: query, variables: argsInput.variables }; } function generateArgsInput(types, args, indent, depth, spacer, indentBy, inputVariables, comments) { var argsInput = { typeVars: '', input: '', variables: {} } if (args.length > 0) { const inputs = []; const typeVars = []; args.forEach(arg => { console.debug(`${' '.repeat(indent)}- generateArgsInput - ${arg.name}: ${generateTypeValue(arg.type)}`); if (inputVariables) { const argVal = getArgValue(types, arg, indent, depth, spacer, indentBy, inputVariables); if (argVal) { argsInput.variables[arg.name] = argVal; typeVars.push(`$${arg.name}: ${generateTypeValue(arg.type)}`); if(comments && arg.description) inputs.push(`# ${arg.description}`); inputs.push(`${arg.name}: $${arg.name}`); } } else { const generatedArg = generateArg(types, arg, indent, depth, spacer, indentBy, inputVariables); if (generatedArg) { if(comments && arg.description) inputs.push(`# ${arg.description}`); inputs.push(generatedArg); } } }); argsInput.typeVars = inputVariables ? ' (' + typeVars.join(', ') + ')' : ''; argsInput.input = `( ${spacer.repeat(indent)}${inputs.join(`,\n${spacer.repeat(indent)}`)} ${spacer.repeat(indent - indentBy)})`; } else console.debug(`${' '.repeat(indent)}- generateArgsInput - args length is 0`); return argsInput; } function generateArg(types, arg, indent, depth, spacer, indentBy, inputVariables) { const argValue = getArgValue(types, arg, indent, depth, spacer, indentBy, inputVariables) if (argValue !== null) return `${spacer.repeat(indent)}${arg.name}: ${argValue}`; return null; } function getArgValue(types, arg, indent, depth, spacer, indentBy, inputVariables) { //needs fix if more kinds which not considered here const kind = getKind(arg.type, []).filter(k => k!= 'NON_NULL')?.[0] || null; const argType = getType(arg.type); console.debug(`${' '.repeat(indent + 1)}- getArgValue - ${arg.name}: ${generateTypeValue(arg.type)}`); const argVal = generateArgValue(types, argType, indent, depth, spacer, indentBy, inputVariables); if (argVal === "") return null; if (kind == 'LIST') if (inputVariables) return [argVal]; else return `[` + argVal + `]`; else return argVal; } function generateArgValue(types, argType, indent, depth, spacer, indentBy, inputVariables) { if (argType.kind == 'SCALAR') return getRandomValue(argType.name, inputVariables); else if (argType.kind == 'ENUM') { const inputTypeDef = types.filter(type => type.name == argType.name && type.kind == 'ENUM')[0]; return inputTypeDef.enumValues[0].name; } else if (argType.kind == 'INPUT_OBJECT') { if (depth <= 0) return ""; var values = [] const inputTypeDef = types.filter(type => type.name == argType.name && type.kind == 'INPUT_OBJECT')[0]; if (inputVariables) { var value = {}; inputTypeDef.inputFields.forEach(f => { const argVal = getArgValue(types, f, indent + indentBy, depth - 1, spacer, indentBy, inputVariables); if (argVal) value[f.name] = argVal; }); return value; } else { inputTypeDef.inputFields.forEach(f => { const generatedArg = generateArg(types, f, indent + indentBy, depth - 1, spacer, indentBy, inputVariables); if (generatedArg) values.push(generatedArg); }); return ` { ${values.join(',\n')} ${spacer.repeat(indent)}}`; } } } function generateTypeValue(type) { const fieldType = getType(type); const fieldKind = getKind(type, []).reverse(); var type = fieldType.name; fieldKind.forEach(k => { if (k == 'LIST') type = '[' + type + ']'; else if (k == 'NON_NULL') type = type + '!'; }); return type; } function getName(responseType) { if (!responseType.name) return getName(responseType.ofType) return responseType.name } function getType(fieldType) { if (!fieldType.name) return getType(fieldType.ofType) return fieldType; } function getKind(fieldType, kinds) { if(fieldType.kind) kinds.push(fieldType.kind); if (fieldType.ofType != null) { return getKind(fieldType.ofType, kinds); } else { return kinds; } } function responseFieldsUnion(types, typeDef, indent, depth, spacer, indentBy) { var fields = []; typeDef.possibleTypes.forEach(pt => { const f = `${spacer.repeat(indent)}... on ${pt.name} { ${generateResponseFields(types, pt, indent + indentBy, depth, spacer, indentBy)} ${spacer.repeat(indent)}}`; fields.push(f); }); return fields.join('\n'); } function responseField(types, f, indent, depth, spacer, indentBy) { console.debug(`${' '.repeat(indent)} - responseField - ${f.name}`); const fieldType = getType(f.type); if (fieldType.kind == 'OBJECT') { var nestedField = generateResponseFields(types, fieldType, indent + indentBy, depth - 1, spacer, indentBy); if (nestedField == "") return null; else return `${f.name} { ${nestedField} ${spacer.repeat(indent)}}`; } else return f.name; } function generateResponseFields(types, responseType, indent, depth, spacer, indentBy) { console.debug(`${' '.repeat(indent)}- generateResponseFields - ${getName(responseType)}`); if (depth <= 0) return ""; const responseTypeDef = types.filter(type => type.name == getName(responseType))[0]; const fields = []; if (responseTypeDef.kind == 'SCALAR') return null; if (responseTypeDef.kind == 'UNION') return responseFieldsUnion(types, responseTypeDef, indent, depth, spacer, indentBy); if (responseTypeDef['fields']) { responseTypeDef.fields.forEach(f => { var field = responseField(types, f, indent, depth, spacer, indentBy); if (field) fields.push(field); }); } return `${spacer.repeat(indent)}` + fields.join(`\n${spacer.repeat(indent)}`); } function getRandomValue(type, inputVariables) { switch (type) { case 'Int': return Math.floor((Math.random() * 1000) + 1); case 'Float': return parseFloat(((Math.random() * 1000) + 1).toFixed(2));; case 'String': return inputVariables ? getRandomString() : '"' + getRandomString() + '"'; case 'uuid': return inputVariables ? uuidv4() : '"' + uuidv4() + '"'; case 'Boolean': return [true, false][Math.floor(Math.random() * 2)]; case 'Date': return inputVariables ? new Date().toDateString() : '"' + new Date().toDateString() + '"'; case 'ID': return inputVariables ? uuidv4() : '"' + uuidv4() + '"'; default: return inputVariables ? "" : "\"\""; } } function getRandomString() { return ['Suspendisse', 'quis', 'gravida', 'risus', 'eu', 'auctor', 'erat', 'Vivamus', 'libero', 'lorem', 'elementum', 'pulvinar', 'lacinia', 'nec', 'accumsan'][Math.floor(Math.random() * 10)]; }