UNPKG

gotql

Version:

A GraphQL query framework for serverside apps

296 lines 11.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.parse = parse; const debug_1 = __importDefault(require("debug")); const ParserError_1 = require("../errors/ParserError"); const generics_1 = require("../types/generics"); const util_1 = require("util"); const shout = (0, debug_1.default)('gotql:errors'); const info = (0, debug_1.default)('gotql:info:parser'); /** * Parses query variables into strings * @param {Object.<string, { type: string, value: string }>} variables Variable object * @return {string} Parsed variables */ function getQueryVars(variables) { info('Getting query variables'); if (!variables || Object.keys(variables).length === 0) return ''; let queryVars = '('; for (const varName in variables) { info('Processing var "%s"', varName); queryVars += `$${varName}: ${variables[varName].type}, `; } return queryVars.slice(0, -2) + ') '; // Split last comma } /** * Parses fields recursively into strings * @param {Array<string | Object.<string, [fieldObj]>>} fieldList List of fields * @return {string} Parsed fields */ function getFields(fieldList, variables) { info('Getting fields'); if (!fieldList) return ''; // Return condition, reached bottom of tree let fieldStr = ''; for (const field of fieldList) { if (typeof field === 'string') { info('Processing field %s', field); fieldStr += `${field} `; // Return plain string field } else if (typeof field === 'object') { /* istanbul ignore next */ const fieldName = Object.keys(field)[0]; info('Processing field %s', fieldName); fieldStr += `${fieldName}${parseArgs(field[fieldName].args, variables)} { ${getFields(field[fieldName].fields, variables)}} `; // Get fields recursively if nested } } info('Parsed field: "%s"', fieldStr); return fieldStr; } /** * Takes all argument-like objects and parse them all into a usable argument declaration * It also checks for variable existence, variable type and parsing * @param argsList {QueryType['operation']['args']} Argument object * @param variables {QueryType['variables']} Variable object */ function parseArgs(argsList, variables) { if (!argsList) return ''; let fieldArgs = ''; info('Parsing args %O', argsList); for (const argName in argsList) { info('Parsing arg "%s"', argName); fieldArgs += `${argName}: ${checkArgs(argsList, argName, variables)}, `; // There'll be a last comma we'll strip later } fieldArgs = `(${fieldArgs.slice(0, -2)})`; // Strips last comma info('Obtained args: %s', fieldArgs); return fieldArgs; } /** * Checks if the value is an usable object. * * An usable object is an object gotQL can use to determine its value. This is an object with keys: { escape: boolean, value: any } * @param {any} varName Object to be checked * @return {boolean} True if is object */ function isUsableObject(varName) { info(`checking if ${JSON.stringify(varName)} is object:`, `type: ${typeof varName}`); const result = !!(typeof varName === 'object' && (varName === null || varName === void 0 ? void 0 : varName.value)); info(`is ${result ? '' : 'not'} an usable object`); return result; } /** * Outputs true if the argument is a boolean * @param {string} varName Variable value * @return {boolean} True if it is a boolean */ function isBool(varValue) { return typeof varValue === typeof true; } /** * Outputs true if the argument is a variable * @param {string|object} varName Variable value or object * @return {boolean} True if it is a variable */ function isVar(varName) { if (isUsableObject(varName) || typeof varName !== 'string') return false; return varName.indexOf('$') === 0; } /** * Parses the variable name into a string to be used. * * If variable is an object, then it is a query variable and must be treated differently * @param {string|object} varName Variable value or object * @return {string} Parsed variablename */ function getParsedVar(varName) { info('Parsing variable "%s" into string', varName); let variable = `"${varName}"`; if (isUsableObject(varName)) variable = `"${varName.value}"`; info('Parsed var "%s" equals to: %s', varName, variable); return variable; } /** * Checks if a given variable is not existent in the query * @param {string} varName Variable name without $ * @param {queryType} query The JSON-Like Query */ function isVarUndefined(varName, variables) { info('Checking if "%s" variable is defined in the query', varName); return !variables || !variables[varName] || !variables[varName].type || variables[varName].value === undefined; } function isArray(value) { const result = Array.isArray(value); info(`Checking if ${JSON.stringify(value)} is an Array: ${result}`); return result; } /** * Check against "Null" only * @param value Argument Value */ function isNull(value) { info(`Checking if ${JSON.stringify(value)} is 'null'`); return value === null; } /** * Check if argument is an object which cannot be used as arg.value * * @param argList List of arguments */ function isArgNestedObject(argList) { info('Checking for nested argument objects without escape or value'); return !isUsableObject(argList) && typeof argList === 'object'; } /** * If an argument list is like: * { * args: { * arg1: { * arg2: string, * arg3: string * } * } * } * * Then this is not an usable object of type { value: any, escape: boolean } which is the main configuration option for gotQL's objects. Thus, we need to iterate over them to build a nested argument list for the spec * * @returns {string} A argument list like argName: { nestedArgName1: "nestedArgValue" } */ function parseNestedArgument(nestedArgList, variables) { let parsedNestedArg = '{ '; for (const argument in nestedArgList) { info('Parsing nested argument "%s"', argument); parsedNestedArg += `${argument}: ${checkArgs(nestedArgList, argument, variables)},`; } // Removing trailing comma, add spaces to commas and final curly braces const parsedFinalString = `${parsedNestedArg.slice(0, -1).replace(',', ', ')} }`; info('Obtained nested arg "%s"', parsedFinalString); return parsedFinalString; } function parseArgsArray(item, variables) { const parsedArray = Object.keys(item).map((key) => ` ${key}: ${checkArgs(item, key, variables)} `); return `{${parsedArray.join(',')}}`; } /** * Checks if the operation argument is a variable or not * * Also checks if the variables are indeed set in the query before using it in the operation * @param {QueryType['operation']['args']} argsList The list of arguments in the query * @param {string} operationArg Argument name * @param {QueryType['variables']} variables Variables list * @returns {string} Parsed operation argument */ function checkArgs(argsList, operationArg, variables) { const argValue = argsList[operationArg]; info(`Obtained arg value of %O for %s`, argValue, operationArg); if (isNull(argValue)) return 'null'; // Check for strict null values (#33) if (isArray(argValue)) { const parsedArray = argValue.map((item) => { const returnByType = { object: (item) => parseArgsArray(item, variables), string: (item) => `"${item}"` }; const fn = returnByType[typeof item]; return fn ? fn(item) : item; }); return `[${parsedArray.join(',')}]`; // Check for array values (#35) } // Issue #28: Allow support for nested arguments if (isArgNestedObject(argValue)) { info('Argument "%s" is a nested argument, parsing...', operationArg); return parseNestedArgument(argValue, variables); } if (isBool(argValue)) return argValue; if (isVar(argValue)) { // Check if is query var, must contain "$" and not be an object const varName = argValue.slice(1); // Removes "$" to check for name info('Argument is variable "%s"', varName); if (isVarUndefined(varName, variables)) throw new Error(`Variable "${varName}" is defined on operation but it has neither a type or a value`); return argValue; } if (isUsableObject(argValue)) { // Check if arg is object (i.e enum) info('Arg is object %O', argValue); if (!argValue.escape) return argValue.value; } return getParsedVar(argValue); } /** * Parses the operation bit of the query * @param {QueryType} query The JSON-Like query to be parsed * @param {boolean} allowEmptyFields If 'true', empty fields are allowed (WARNING: this is not considered a good practice) * @return {string} Parsed operation query */ function parseOperation(query, allowEmptyFields = false) { const operation = query.operation; const hasEmptyFields = !operation.fields || !operation.fields.length; info('Parsing operation %s', operation.name); if (!operation.name) throw new Error(`name is required for graphQL operation`); if (hasEmptyFields && !allowEmptyFields) throw new Error(`field list is required for operation "${operation.name}"`); if (hasEmptyFields) info('Hint: Returning no fields is not considered a good practice'); try { let operationArgs = ''; if (operation.args && Object.keys(operation.args).length > 0) { info('Parsing args'); operationArgs = parseArgs(query.operation.args, query.variables); info('Operation args are: %s', operationArgs); } const alias = operation.alias ? `${operation.alias}:` : ''; const parsedOperation = hasEmptyFields ? `${alias} ${operation.name}${operationArgs}`.trim() : `${alias} ${operation.name}${operationArgs} { ${getFields(operation.fields, query.variables).trim()} }`.trim(); info('Parsed operation: %s', parsedOperation); return parsedOperation; } catch (error) { shout('Parser error on operation: %O', error); if (error instanceof Error) throw new Error(`Failed to parse operation "${query.operation.name}" => ${error.message}`); throw new Error(`Unknown error when parsing operation: ${(0, util_1.inspect)(error)}`); } } function getFragments(fragments) { if (!fragments) return ''; return ` ${fragments.map((f) => f.trim()).join('\n')}`; } /** * Parses a JSON-like query into a string * @param {queryType} query The JSON-like query to be parsed * @param {string} type Can be 'query' or 'mutation' * @return {string} Parsed query */ function parse(query, type) { info('Starting parser with query %O', query); try { if (!query.operation) throw new Error('a query must have at least one operation'); if (!type) throw new Error('type must be either "query" or "mutation"'); const queryName = query.name ? `${query.name} ` : ''; info('Defining name "%s"', queryName); const parsedQuery = `${type.trim()} ${queryName}${getQueryVars(query.variables)}{ ${parseOperation(query, type === generics_1.GotQL.ExecutionType.MUTATION)} }${getFragments(query.fragments)}`.trim(); info('Parsed query: %s', parsedQuery); return parsedQuery; } catch (error) { shout('Parser error: %O', error); if (error instanceof Error) throw new ParserError_1.ParserError(error.message); throw new ParserError_1.ParserError(`Unknown error: ${(0, util_1.inspect)(error)}`); } } //# sourceMappingURL=parser.js.map