UNPKG

gql-generator

Version:

Generate queries from graphql schema, used for writing api test.

285 lines (263 loc) 9.97 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const program = require('commander'); const { Source, buildSchema } = require('graphql'); const { rimrafSync } = require('rimraf'); function main({ schemaFilePath, destDirPath, depthLimit = 100, includeDeprecatedFields = false, fileExtension, assumeValid, includeCrossReferences = false, } = {}) { let assume = false; if (assumeValid === 'true') { assume = true; } const typeDef = fs.readFileSync(schemaFilePath, 'utf-8'); const source = new Source(typeDef); const gqlSchema = buildSchema(source, { assumeValidSDL: assume }); rimrafSync(destDirPath); path.resolve(destDirPath).split(path.sep).reduce((before, cur) => { const pathTmp = path.join(before, cur + path.sep); if (!fs.existsSync(pathTmp)) { fs.mkdirSync(pathTmp); } return path.join(before, cur + path.sep); }, ''); let indexJsExportAll = ''; /** * Compile arguments dictionary for a field * @param field current field object * @param duplicateArgCounts map for deduping argument name collisions * @param allArgsDict dictionary of all arguments */ const getFieldArgsDict = ( field, duplicateArgCounts, allArgsDict = {}, ) => field.args.reduce((o, arg) => { if (arg.name in duplicateArgCounts) { const index = duplicateArgCounts[arg.name] + 1; duplicateArgCounts[arg.name] = index; o[`${arg.name}${index}`] = arg; } else if (allArgsDict[arg.name]) { duplicateArgCounts[arg.name] = 1; o[`${arg.name}1`] = arg; } else { o[arg.name] = arg; } return o; }, {}); /** * Generate variables string * @param dict dictionary of arguments */ const getArgsToVarsStr = (dict) => Object.entries(dict) .map(([varName, arg]) => `${arg.name}: $${varName}`) .join(', '); /** * Generate types string * @param dict dictionary of arguments */ const getVarsToTypesStr = (dict) => Object.entries(dict) .map(([varName, arg]) => `$${varName}: ${arg.type}`) .join(', '); /** * Generate the query for the specified field * @param curName name of the current field * @param curParentType parent type of the current field * @param curParentName parent name of the current field * @param argumentsDict dictionary of arguments from all fields * @param duplicateArgCounts map for deduping argument name collisions * @param crossReferenceKeyList list of the cross reference * @param curDepth current depth of field * @param fromUnion adds additional depth for unions to avoid empty child */ const generateQuery = ( curName, curParentType, curParentName, argumentsDict = {}, duplicateArgCounts = {}, crossReferenceKeyList = [], // [`${curParentName}To${curName}Key`] curDepth = 1, fromUnion = false, ) => { const field = gqlSchema.getType(curParentType).getFields()[curName]; const curTypeName = field.type.toJSON().replace(/[[\]!]/g, ''); const curType = gqlSchema.getType(curTypeName); let queryStr = ''; let childQuery = ''; if (curType.getFields) { const crossReferenceKey = `${curParentName}To${curName}Key`; if ( (!includeCrossReferences && crossReferenceKeyList.indexOf(crossReferenceKey) !== -1) || (fromUnion ? curDepth - 2 : curDepth) > depthLimit ) { return ''; } crossReferenceKeyList.push(crossReferenceKey); const childKeys = Object.keys(curType.getFields()); childQuery = childKeys .filter((fieldName) => { /* Exclude deprecated fields */ const fieldSchema = gqlSchema.getType(curType).getFields()[fieldName]; return includeDeprecatedFields || !fieldSchema.deprecationReason; }) .map((cur) => generateQuery( cur, curType, curName, argumentsDict, duplicateArgCounts, crossReferenceKeyList, curDepth + 1, fromUnion, ).queryStr) .filter((cur) => Boolean(cur)) .join('\n'); } if (!(curType.getFields && !childQuery)) { queryStr = `${' '.repeat(curDepth)}${field.name}`; if (field.args.length > 0) { const dict = getFieldArgsDict(field, duplicateArgCounts, argumentsDict); Object.assign(argumentsDict, dict); queryStr += `(${getArgsToVarsStr(dict)})`; } if (childQuery) { queryStr += `{\n${childQuery}\n${' '.repeat(curDepth)}}`; } } /* Union types */ if (curType.astNode && curType.astNode.kind === 'UnionTypeDefinition') { const types = curType.getTypes(); if (types && types.length) { const indent = `${' '.repeat(curDepth)}`; const fragIndent = `${' '.repeat(curDepth + 1)}`; queryStr += '{\n'; queryStr += `${fragIndent}__typename\n`; for (let i = 0, len = types.length; i < len; i++) { const valueTypeName = types[i]; const valueType = gqlSchema.getType(valueTypeName); const unionChildQuery = Object.keys(valueType.getFields()) .map((cur) => generateQuery( cur, valueType, curName, argumentsDict, duplicateArgCounts, crossReferenceKeyList, curDepth + 2, true, ).queryStr) .filter((cur) => Boolean(cur)) .join('\n'); /* Exclude empty unions */ if (unionChildQuery) { queryStr += `${fragIndent}... on ${valueTypeName} {\n${unionChildQuery}\n${fragIndent}}\n`; } } queryStr += `${indent}}`; } } return { queryStr, argumentsDict }; }; /** * Generate the query for the specified field * @param obj one of the root objects(Query, Mutation, Subscription) * @param description description of the current object */ const generateFile = (obj, description) => { let indexJs = 'const fs = require(\'fs\');\nconst path = require(\'path\');\n\n'; let outputFolderName; switch (true) { case /Mutation.*$/.test(description): case /mutation.*$/.test(description): outputFolderName = 'mutations'; break; case /Query.*$/.test(description): case /query.*$/.test(description): outputFolderName = 'queries'; break; case /Subscription.*$/.test(description): case /subscription.*$/.test(description): outputFolderName = 'subscriptions'; break; default: console.log('[gqlg warning]:', 'description is required'); } const writeFolder = path.join(destDirPath, `./${outputFolderName}`); try { fs.mkdirSync(writeFolder); } catch (err) { if (err.code !== 'EEXIST') throw err; } Object.keys(obj).forEach((type) => { const field = gqlSchema.getType(description).getFields()[type]; /* Only process non-deprecated queries/mutations: */ if (includeDeprecatedFields || !field.deprecationReason) { const queryResult = generateQuery(type, description); const varsToTypesStr = getVarsToTypesStr(queryResult.argumentsDict); let query = queryResult.queryStr; let queryName; switch (true) { case /Mutation/.test(description): case /mutation/.test(description): queryName = 'mutation'; break; case /Query/.test(description): case /query/.test(description): queryName = 'query'; break; case /Subscription/.test(description): case /subscription/.test(description): queryName = 'subscription'; break; default: break; } query = `${queryName || description.toLowerCase()} ${type}${varsToTypesStr ? `(${varsToTypesStr})` : ''}{\n${query}\n}`; fs.writeFileSync(path.join(writeFolder, `./${type}.${fileExtension}`), query); indexJs += `module.exports.${type} = fs.readFileSync(path.join(__dirname, '${type}.${fileExtension}'), 'utf8');\n`; } }); fs.writeFileSync(path.join(writeFolder, 'index.js'), indexJs); indexJsExportAll += `module.exports.${outputFolderName} = require('./${outputFolderName}');\n`; }; if (gqlSchema.getMutationType()) { generateFile(gqlSchema.getMutationType().getFields(), gqlSchema.getMutationType().name); } else { console.log('[gqlg warning]:', 'No mutation type found in your schema'); } if (gqlSchema.getQueryType()) { generateFile(gqlSchema.getQueryType().getFields(), gqlSchema.getQueryType().name); } else { console.log('[gqlg warning]:', 'No query type found in your schema'); } if (gqlSchema.getSubscriptionType()) { generateFile(gqlSchema.getSubscriptionType().getFields(), gqlSchema.getSubscriptionType().name); } else { console.log('[gqlg warning]:', 'No subscription type found in your schema'); } fs.writeFileSync(path.join(destDirPath, 'index.js'), indexJsExportAll); } module.exports = main; if (require.main === module) { program .name('gqlg') .option('--schemaFilePath [value]', 'path of your graphql schema file') .option('--destDirPath [value]', 'dir you want to store the generated queries') .option('--depthLimit [value]', 'query depth you want to limit (The default is 100)') .option('--assumeValid [value]', 'assume the SDL is valid (The default is false)') .option('--ext [value]', 'extension file to use', 'gql') .option('-C, --includeDeprecatedFields [value]', 'Flag to include deprecated fields (The default is to exclude)') .option('-R, --includeCrossReferences', 'Flag to include fields that have been added to parent queries already (The default is to exclude)') .showHelpAfterError() .parse(process.argv); const { ext, ...opts } = program.opts(); main({ ...opts, fileExtension: ext }); }