import { EnumType } from './types/EnumType'; import { VariableType } from './types/VariableType'; export const configFields = [ '__args', '__alias', '__aliasFor', '__variables', '__directives', '__on', '__all_on', '__typeName', '__name', ]; function stringify(obj_from_json: any): string { if (obj_from_json instanceof EnumType) { return obj_from_json.value; } // variables should be prefixed with dollar sign and not quoted else if (obj_from_json instanceof VariableType) { return `$${obj_from_json.value}`; } // Cheers to Derek: https://stackoverflow.com/questions/11233498/json-stringify-without-quotes-on-properties else if (typeof obj_from_json !== 'object' || obj_from_json === null) { // not an object, stringify using native function return JSON.stringify(obj_from_json); } else if (Array.isArray(obj_from_json)) { return `[${obj_from_json.map((item) => stringify(item)).join(', ')}]`; } // Implements recursive object serialization according to JSON spec // but without quotes around the keys. const props: string = Object.keys(obj_from_json) .map((key) => `${key}: ${stringify(obj_from_json[key])}`) .join(', '); return `{${props}}`; } function buildArgs(argsObj: any): string { const args = []; for (const argName in argsObj) { args.push(`${argName}: ${stringify(argsObj[argName])}`); } return args.join(', '); } function buildVariables(varsObj: any): string { const args = []; for (const varName in varsObj) { args.push(`$${varName}: ${varsObj[varName]}`); } return args.join(', '); } function buildDirectives(dirsObj: any): string { const directiveName = Object.keys(dirsObj)[0]; const directiveValue = dirsObj[directiveName]; if ( typeof directiveValue === 'boolean' || (typeof directiveValue === 'object' && Object.keys(directiveValue).length === 0) ) { return directiveName; } else if (typeof directiveValue === 'object') { const args = []; for (const argName in directiveValue) { const argVal = stringify(directiveValue[argName]).replace(/"/g, ''); args.push(`${argName}: ${argVal}`); } return `${directiveName}(${args.join(', ')})`; } else { throw new Error( `Unsupported type for directive: ${typeof directiveValue}. Types allowed: object, boolean.\n` + `Offending object: ${JSON.stringify(dirsObj)}` ); } } function getIndent(level: number): string { return Array(level * 4 + 1).join(' '); } function filterNonConfigFields(fieldName: string, ignoreFields: string[]) { // Returns true if fieldName is not a 'configField'. return ( configFields.indexOf(fieldName) == -1 && ignoreFields.indexOf(fieldName) == -1 ); } function convertQuery( node: any, level: number, output: [string, number][], options: IJsonToGraphQLOptions ) { Object.keys(node) .filter((key) => filterNonConfigFields(key, options.ignoreFields!)) .forEach((key) => { let value = node[key]; if (typeof value === 'object') { if (Array.isArray(value)) { value = value.find( (item) => item && typeof item === 'object' ); if (!value) { output.push([`${key}`, level]); return; } } // Check if the object would be empty if ( value && Object.keys(value).filter( (k) => value[k] !== false || options.includeFalsyKeys ).length === 0 ) { // If so, we don't include it into the query return; } const fieldCount = Object.keys(value).filter((keyCount) => filterNonConfigFields(keyCount, options.ignoreFields!) ).length; const subFields = fieldCount > 0; const argsExist = typeof value.__args === 'object' && Object.keys(value.__args).length > 0; const directivesExist = typeof value.__directives === 'object'; const fullFragmentsExist = value.__all_on instanceof Array; const partialFragmentsExist = typeof value.__on === 'object'; let token = `${key}`; if (typeof value.__name === 'string') { token = `${token} ${value.__name}`; } if (typeof value.__aliasFor === 'string') { token = `${token}: ${value.__aliasFor}`; } if ( typeof value.__variables === 'object' && Object.keys(value.__variables).length > 0 ) { token = `${token} (${buildVariables(value.__variables)})`; } else if (argsExist || directivesExist) { let argsStr = ''; let dirsStr = ''; if (directivesExist) { dirsStr = Object.entries(value.__directives) .map( (item) => `@${buildDirectives({ [item[0]]: item[1] })}` ) .join(' '); } if (argsExist) { argsStr = `(${buildArgs(value.__args)})`; } const spacer = directivesExist && argsExist ? ' ' : ''; token = `${token} ${argsStr}${spacer}${dirsStr}`; } output.push([ token + (subFields || partialFragmentsExist || fullFragmentsExist ? ' {' : ''), level, ]); convertQuery(value, level + 1, output, options); if (fullFragmentsExist) { value.__all_on.forEach((fullFragment: string) => { output.push([`...${fullFragment}`, level + 1]); }); } if (partialFragmentsExist) { const inlineFragments: { __typeName: string }[] = value.__on instanceof Array ? value.__on : [value.__on]; inlineFragments.forEach((inlineFragment) => { const name = inlineFragment.__typeName; output.push([`... on ${name} {`, level + 1]); convertQuery( inlineFragment, level + 2, output, options ); output.push(['}', level + 1]); }); } if (subFields || partialFragmentsExist || fullFragmentsExist) { output.push(['}', level]); } } else if (options.includeFalsyKeys === true || value) { output.push([`${key}`, level]); } }); } export interface IJsonToGraphQLOptions { pretty?: boolean; ignoreFields?: string[]; includeFalsyKeys?: boolean; } export function jsonToGraphQLQuery( query: any, options: IJsonToGraphQLOptions = {} ) { if (!query || typeof query != 'object') { throw new Error('query object not specified'); } if (Object.keys(query).length == 0) { throw new Error('query object has no data'); } if (!(options.ignoreFields instanceof Array)) { options.ignoreFields = []; } let queryLines: [string, number][] = []; convertQuery(query, 0, queryLines, options); queryLines = dropEmptyObjects(queryLines); let output = ''; queryLines.forEach(([line, level]) => { if (options.pretty) { if (output) { output += '\n'; } output += getIndent(level) + line; } else { if (output) { output += ' '; } output += line; } }); return output; } type InputItem = [string, number]; type OutputItem = [string, number]; interface IContext { indent: number; text: string | null; isEmpty: boolean; contents: Array; } function dropEmptyObjects(inputList: Array): Array { const rootContext: IContext = { indent: -1, text: null, isEmpty: true, contents: [], }; const stack: IContext[] = [rootContext]; for (let [text, indent] of inputList) { text = text.trim(); if (text.endsWith('{')) { const context: IContext = { indent: indent, text: text, isEmpty: true, contents: [], }; stack[stack.length - 1].contents.push(context); stack.push(context); } else if (text === '}') { const context = stack.pop()!; if (context.isEmpty) { // Remove the context from its parent's contents stack[stack.length - 1].contents.pop(); } else { // Mark the parent as not empty stack[stack.length - 1].isEmpty = false; } } else { // It's a field, add it to the current context stack[stack.length - 1].contents.push(text); stack[stack.length - 1].isEmpty = false; } } const output: OutputItem[] = []; function traverse(context: IContext): void { for (const item of context.contents) { if (typeof item === 'object') { // It's a context output.push([item.text!, item.indent]); traverse(item); output.push(['}', item.indent]); } else { // It's a field (string) output.push([item, context.indent + 1]); } } } traverse(rootContext); return output; }