UNPKG

@aws/neptune-for-graphql

Version:

CLI utility to create and maintain a GraphQL API for Amazon Neptune

1,052 lines (848 loc) 41.6 kB
/* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0 or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ const gql = require('graphql-tag'); // GraphQL library to parse the GraphQL query const useCallSubquery = false; // TIMESTAMP HERE const schemaDataModelJSON = `INSERT SCHEMA DATA MODEL HERE`; const schemaDataModel = JSON.parse(schemaDataModelJSON); function resolveGraphDBQueryFromAppSyncEvent(event) { let query = '{\n'; let args = ''; Object.keys(event.arguments).forEach(key => { if (typeof event.arguments[key] === 'object') { args += key + ': {'; let obj = event.arguments[key]; Object.keys(obj).forEach(key2 => { args += key2 + ': "' + obj[key2] + '", ' }); args = args.substring(0, args.length - 2); args += '}'; } else { args += key + ': "' + event.arguments[key] + '", ' args = args.substring(0, args.length - 2); } }); if (args != '') { query += event.field + '(' + args + ') '; } else { query += event.field + ' '; } query += event.selectionSetGraphQL; query += '\n}'; const graphQuery = resolveGraphDBQuery(query); return graphQuery; } function resolveGraphDBQueryFromApolloQueryEvent(event) { // TODO } const matchStatements = []; // openCypher match statements const withStatements = []; // openCypher with statements const returnString = []; // openCypher return statements let parameters = {}; // openCypher query parameters function getTypeAlias(typeName) { let alias = null; schemaDataModel.definitions.forEach(def => { if (def.kind === 'ObjectTypeDefinition') { if (def.name.value == typeName) { if (def.directives.length > 0) { def.directives.forEach(directive => { if (directive.name.value === 'alias') { alias = directive.arguments[0].value.value; } }); } } } }); if (alias == null) return typeName else return alias; } function getSchemaInputTypeArgs (inputType, schemaInfo) { schemaDataModel.definitions.forEach(def => { if (def.kind === 'InputObjectTypeDefinition') { if (def.name.value == inputType) { def.fields.forEach(field => { let arg = {name: '', type:''}; let alias = null; arg.name = field.name.value; if (field.type.kind === 'ListType') { arg.type = field.type.type.name.value; } if (field.type.kind === 'NamedType') { arg.type = field.type.name.value; } if (field.type.kind === 'NonNullType') { arg.type = field.type.type.name.value; } if (field.directives.length > 0) { field.directives.forEach(directive => { if (directive.name.value === 'alias') { alias = directive.arguments[0].value.value; } if (directive.name.value === 'id') { schemaInfo.graphDBIdArgName = arg.name; } }); } if (alias != null) Object.assign(arg, {alias: alias}); schemaInfo.args.push(arg); }); } } }); } function getSchemaQueryInfo(name) { const r = { type: '', // rename functionType name: name, returnType: '', returnTypeAlias: '', pathName: '', returnIsArray: false, graphQuery: null, args: [], graphDBIdArgName: '', argOptionsLimit: null, argOptionsOffset: null, argOptionsOrderBy: null, }; schemaDataModel.definitions.forEach(def => { if (def.kind != 'ObjectTypeDefinition') { return; } if (!(def.name.value === 'Query' || def.name.value === 'Mutation')) { return; } def.fields.forEach(field => { if (field.name.value != name) { return; } r.type = def.name.value; r.name = field.name.value; // Return type if (field.type.kind === 'ListType') { r.returnIsArray = true; r.returnType = field.type.type.name.value; } if (field.type.kind === 'NamedType') { r.returnIsArray = false; r.returnType = field.type.name.value; } if (field.type.kind === 'NonNullType') { if (field.type.type.kind === 'NamedType') { r.returnIsArray = false; r.returnType = field.type.type.name.value; } } r.returnTypeAlias = getTypeAlias(r.returnType); r.pathName = r.name + '_' + r.returnType; // graphQuery if (field.directives.length > 0) { field.directives.forEach(directive => { if (directive.name.value === 'graphQuery' || directive.name.value === 'Cypher' || directive.name.value === 'cypher') r.graphQuery = directive.arguments[0].value.value; }); } // args if (field.arguments.length > 0) { field.arguments.forEach(arg => { if (arg.type.kind === 'NamedType') { getSchemaInputTypeArgs(arg.type.name.value, r); } else if (arg.type.kind === 'NonNullType') { getSchemaInputTypeArgs(arg.type.type.name.value, r); } else if (arg.type.type.name.value === 'String' || arg.type.type.name.value === 'Int' || arg.type.type.name.value === 'ID') { r.args.push({name: arg.name.value, type: arg.type.type.name.value}); } else { // GraphQL type input } }); } }); }); if (r.returnType == '') { console.error('GraphQL query not found.'); } return r; } function getSchemaTypeInfo(lastTypeName, typeName, pathName) { const r = { name: typeName, type: '', typeAlias: '', pathName: pathName + '_' + typeName, isArray: false, isRelationship: false, relationship: {edgeType: '', direction: 'IN'}, graphQuery: null }; schemaDataModel.definitions.forEach(def => { if (def.kind === 'ObjectTypeDefinition') { if (def.name.value === lastTypeName) { def.fields.forEach(field => { if (field.name.value === typeName) { // isArray if (field.type.kind === 'ListType') { r.isArray = true; r.type = field.type.type.name.value; } if (field.type.kind === 'NamedType') { r.isArray = false; r.type = field.type.name.value; } // isRelationship if (field.directives.length > 0) { field.directives.forEach(directive => { if (directive.name.value === 'relationship') { r.isRelationship = true; directive.arguments.forEach(arg => { if (arg.name.value === 'type' || arg.name.value === 'edgeType') { r.relationship.edgeType = arg.value.value; } if (arg.name.value === 'direction') { r.relationship.direction = arg.value.value; } }); } }); } } }); } } }); r.typeAlias = getTypeAlias(r.type); return r; } function getSchemaFieldInfo(typeName, fieldName, pathName) { const r = { name: fieldName, alias: '', type: '', isSchemaType: false, pathName: '', isId: false, isArray: false, isRequired: false, graphQuery: null, relationship: null, args:[], graphDBIdArgName: '', argOptionsLimit: null, argOptionsOffset: null, argOptionsOrderBy: null, } schemaDataModel.definitions.forEach(def => { if (def.kind === 'ObjectTypeDefinition') { if (def.name.value === typeName) { def.fields.forEach(field => { if (field.name.value === fieldName) { r.name = field.name.value; r.alias = r.name; if (field.type.kind === 'ListType') { r.isArray = true; r.type = field.type.type.name.value; } if (field.type.kind === 'NamedType') { r.isArray = false; r.type = field.type.name.value; } if (field.type.kind === 'NonNullType') { r.isArray = false; r.type = field.type.type.name.value; } r.pathName = pathName + '_' + r.name; if (field.directives.length > 0) { field.directives.forEach(directive => { if (directive.name.value === 'alias') { r.alias = directive.arguments[0].value.value; } if (directive.name.value === 'graphQuery' || directive.name.value === 'Cypher' || directive.name.value === 'cypher') { r.graphQuery = directive.arguments[0].value.value; if (fieldName == 'id') { r.graphQuery = r.graphQuery.replace(' as id', ''); r.graphQuery = r.graphQuery.replace(' AS id', ''); } } if (directive.name.value === 'id') r.graphDBIdArgName = r.name; }); } if (field.arguments.length > 0) { field.arguments.forEach(arg => { if (arg.type.kind === 'NamedType') { getSchemaInputTypeArgs(arg.type.name.value, r); } else if (arg.type.kind === 'NonNullType') { getSchemaInputTypeArgs(arg.type.type.name.value, r); } else if (arg.type.type.name.value === 'String' || arg.type.type.name.value === 'Int' || arg.type.type.name.value === 'ID') { r.args.push({name: arg.name.value, type: arg.type.type.name.value}); } else { // GraphQL type input } }); } } }); } } }); schemaDataModel.definitions.forEach(def => { if (def.kind === 'ObjectTypeDefinition') { if (def.name.value === r.type) { r.isSchemaType = true; } } }); if (r.type == '') { console.error('GraphQL field not found.'); } return r; } function getOptionsInSchemaInfo(fields, schemaInfo) { fields.forEach( field => { if (field.name.value == 'limit') { schemaInfo.argOptionsLimit = field.value.value; } /* TODO if (field.name.value == 'offset') { schemaInfo.argOptionsOffset = field.value.value; } if (field.name.value == 'orderBy') { schemaInfo.argOptionsOrderBy = field.value.value; } */ }); } function createQueryFunctionMatchStatement(obj, matchStatements, querySchemaInfo) { if (querySchemaInfo.graphQuery != null) { var gq = querySchemaInfo.graphQuery.replaceAll('this', querySchemaInfo.pathName); obj.definitions[0].selectionSet.selections[0].arguments.forEach(arg => { gq = gq.replace('$' + arg.name.value, arg.value.value); }); matchStatements.push(gq); } else { let { queryArguments, where } = getQueryArguments(obj.definitions[0].selectionSet.selections[0].arguments, querySchemaInfo); if (queryArguments.length > 0) { matchStatements.push(`MATCH (${querySchemaInfo.pathName}:\`${querySchemaInfo.returnTypeAlias}\`{${queryArguments}})${where}`); } else { matchStatements.push(`MATCH (${querySchemaInfo.pathName}:\`${querySchemaInfo.returnTypeAlias}\`)${where}`); } if (querySchemaInfo.argOptionsLimit != null) matchStatements.push(`WITH ${querySchemaInfo.pathName} LIMIT ${querySchemaInfo.argOptionsLimit}`); } withStatements.push({carryOver: querySchemaInfo.pathName, inLevel:'', content:''}); } function getQueryArguments(args, querySchemaInfo) { let where = ''; let queryArguments = ''; args.forEach(arg => { if (arg.name.value == 'filter') { let inputFields = transformFunctionInputParameters(arg.value.fields, querySchemaInfo); queryArguments = queryArguments + inputFields.fields + ","; if (inputFields.graphIdValue != null) { let param = querySchemaInfo.pathName + '_' + 'whereId'; Object.assign(parameters, { [param]: inputFields.graphIdValue }); where = ` WHERE ID(${querySchemaInfo.pathName}) = $${param}`; } } else if (arg.name.value == 'options') { if (arg.value.kind === 'ObjectValue') getOptionsInSchemaInfo(arg.value.fields, querySchemaInfo); } else { queryArguments = queryArguments + arg.name.value + ":'" + arg.value.value + "',"; } }); queryArguments = queryArguments.substring(0, queryArguments.length - 1); return { queryArguments, where }; } function extractTextBetweenParentheses(str) { const match = str.match(/\(([^)]+)\)/); return match ? match[1] : ''; // Returns the content between the parentheses } function modifyVariableNames(query, name) { return query.replace(/\b(\w+)\b/g, function (match, p1, offset, string) { // Check if the matched word is preceded by '(', '[', '[:', or '(:' if ( string[offset - 1] === '(' || string[offset - 1] === '[' || (string[offset - 2] === '[' && string[offset - 1] === ':') || (string[offset - 2] === '(' && string[offset - 1] === ':') ) { return name + '_' + p1; } return match; }); } function graphQueryRefactoring(lastNamePath, fieldSchemaInfo) { const r = { queryMatch:'', returnCarryOver: '', inLevel : '', returnAggregation: ''} const name = lastNamePath + '_' + fieldSchemaInfo.name; const statementParts = fieldSchemaInfo.graphQuery.split(' RETURN '); const returnStatement = statementParts[1]; r.queryMatch = statementParts[0]; r.queryMatch = modifyVariableNames(r.queryMatch, name); r.queryMatch = r.queryMatch.replace(name +'_this', lastNamePath); let returningName = ''; let isAggregation = false; //check if includes aggregating functions if (returnStatement.includes('(')) { returningName = extractTextBetweenParentheses(returnStatement); isAggregation = true; } else { returningName = returnStatement; } if (isAggregation) { r.returnAggregation = returnStatement.replace(returningName, name + '_' + returningName); r.inLevel = name; r.returnCarryOver = name + '_' + returningName; } else { r.returnCarryOver = name + '_' + returningName; } return r; } function createQueryFieldMatchStatement(fieldSchemaInfo, lastNamePath) { // solution until CALL subquery is supported in Neptune openCypher const refactored = graphQueryRefactoring(lastNamePath, fieldSchemaInfo); if (refactored.queryMatch.toUpperCase().includes('MATCH')) refactored.queryMatch = 'OPTIONAL ' + refactored.queryMatch; matchStatements.push(refactored.queryMatch); if ( refactored.returnAggregation != '' ) { const thisWithId = withStatements.push({carryOver: refactored.returnCarryOver, inLevel: '', content: `${refactored.returnAggregation} AS ${refactored.inLevel}`}) -1; let i = withStatements.findIndex(({carryOver}) => carryOver.startsWith(lastNamePath)); withStatements[i].content += refactored.inLevel; for (let p = thisWithId -1; p > i; p--) { withStatements[p].inLevel += refactored.inLevel + ', '; } } else { // no new with, just add it to lastnamepath content // maybe not needed } } function createQueryFieldLeafStatement(fieldSchemaInfo, lastNamePath) { let i = withStatements.findIndex(({carryOver}) => carryOver.startsWith(lastNamePath)); if (withStatements[i].content.slice(-2) != ', ' && withStatements[i].content.slice(-1) != '{' && withStatements[i].content != '' ) withStatements[i].content += ', '; withStatements[i].content += fieldSchemaInfo.name + ':'; if (fieldSchemaInfo.graphDBIdArgName === fieldSchemaInfo.name && fieldSchemaInfo.graphQuery == null) { withStatements[i].content += 'ID(' + lastNamePath + ')'; } else { if (fieldSchemaInfo.graphQuery !=null ) { if (useCallSubquery) { matchStatements.push(` CALL { WITH ${lastNamePath} ${fieldSchemaInfo.graphQuery.replaceAll('this', lastNamePath)} AS ${lastNamePath + '_' + fieldSchemaInfo.name} }`); withStatements[i].content += ' ' + lastNamePath + '_' + fieldSchemaInfo.name; } else { createQueryFieldMatchStatement(fieldSchemaInfo, lastNamePath); } } else { withStatements[i].content += ' ' + lastNamePath + '.' + `\`${fieldSchemaInfo.alias}\``; } } } function createTypeFieldStatementAndRecurse(e, fieldSchemaInfo, lastNamePath, lastType) { const schemaTypeInfo = getSchemaTypeInfo(lastType, fieldSchemaInfo.name, lastNamePath); // check if the field has is a function with parameters, look for filters and options if (e.arguments !== undefined) { e.arguments.forEach(arg => { if (arg.value.kind === 'ObjectValue' && arg.name.value === 'options') getOptionsInSchemaInfo(arg.value.fields, fieldSchemaInfo); }); } let { queryArguments, where } = getQueryArguments(e.arguments, fieldSchemaInfo); if (queryArguments != '') queryArguments = '{' + queryArguments + '}'; if (schemaTypeInfo.isRelationship) { if (schemaTypeInfo.relationship.direction === 'IN') { matchStatements.push(`OPTIONAL MATCH (${lastNamePath})<-[${schemaTypeInfo.pathName}_${schemaTypeInfo.relationship.edgeType}:${schemaTypeInfo.relationship.edgeType}]-(${schemaTypeInfo.pathName}:\`${schemaTypeInfo.typeAlias}\`${queryArguments})`); } else { matchStatements.push(`OPTIONAL MATCH (${lastNamePath})-[${schemaTypeInfo.pathName}_${schemaTypeInfo.relationship.edgeType}:${schemaTypeInfo.relationship.edgeType}]->(${schemaTypeInfo.pathName}:\`${schemaTypeInfo.typeAlias}\`${queryArguments})`); } } const thisWithId = withStatements.push({carryOver: schemaTypeInfo.pathName, inLevel: '', content: ''}) - 1; if (schemaTypeInfo.isArray) { withStatements[thisWithId].content += 'collect('; } withStatements[thisWithId].content += '{'; selectionsRecurse(e.selectionSet.selections, schemaTypeInfo.pathName, schemaTypeInfo.type); withStatements[thisWithId].content += '}'; if (schemaTypeInfo.isArray) { if (fieldSchemaInfo.argOptionsLimit != null) { withStatements[thisWithId].content += `)[..${fieldSchemaInfo.argOptionsLimit}] AS ${schemaTypeInfo.pathName}_collect`; } else { withStatements[thisWithId].content += ') AS ' + schemaTypeInfo.pathName + '_collect'; } let i = withStatements.findIndex(({carryOver}) => carryOver.startsWith(lastNamePath)); if (withStatements[i].content.slice(-2) != ', ' && withStatements[i].content.slice(-1) != '{') withStatements[i].content += ', '; withStatements[i].content += schemaTypeInfo.name + ': ' + schemaTypeInfo.pathName + '_collect'; for (let p = thisWithId -1; p > i; p--) { withStatements[p].inLevel += schemaTypeInfo.pathName + '_collect, '; } } else { withStatements[thisWithId].content += ' AS ' + schemaTypeInfo.pathName + '_one'; let i = withStatements.findIndex(({carryOver}) => carryOver.startsWith(lastNamePath)); if (withStatements[i].content.slice(-2) != ', ' && withStatements[i].content.slice(-1) != '{') withStatements[i].content += ', '; withStatements[i].content += schemaTypeInfo.name + ': ' + schemaTypeInfo.pathName + '_one'; for (let p = thisWithId -1; p > i; p--) { withStatements[p].inLevel += schemaTypeInfo.pathName + '_one, '; } } } function selectionsRecurse(s, lastNamePath, lastType) { s.forEach(e => { const fieldSchemaInfo = getSchemaFieldInfo(lastType, e.name.value, lastNamePath); // check if is schema type if (!fieldSchemaInfo.isSchemaType) { createQueryFieldLeafStatement(fieldSchemaInfo, lastNamePath); // exit terminating recursion branch return } createTypeFieldStatementAndRecurse(e, fieldSchemaInfo, lastNamePath, lastType) }); }; function finalizeGraphQuery(matchStatements, withStatements, returnString) { // make a string out of match statements let ocMatchStatements = ''; matchStatements.forEach(e => { ocMatchStatements += e + '\n'; }); ocMatchStatements = ocMatchStatements.substring(0, ocMatchStatements.length - 1); let ocWithStatements = ''; let carryOvers = ''; let withToReverse = []; for (let i = 1; i < withStatements.length; i++) { carryOvers += withStatements[i - 1].carryOver + ', '; withToReverse.push('\n' + 'WITH ' + carryOvers + withStatements[i].inLevel + withStatements[i].content); } for(let i = withToReverse.length - 1; i >= 0; i--) { ocWithStatements += withToReverse[i]; } // make a string out of return statement let ocReturnStatement = ''; returnString.forEach(e => { ocReturnStatement = ocReturnStatement + e; }); // make the oc query string return ocMatchStatements + ocWithStatements + '\nRETURN ' + ocReturnStatement; } function resolveGrapgDBqueryForGraphQLQuery (obj, querySchemaInfo) { createQueryFunctionMatchStatement(obj, matchStatements, querySchemaInfo); // start processing the given query if (querySchemaInfo.returnIsArray) { returnString.push('collect('); } withStatements[0].content = '{'; selectionsRecurse(obj.definitions[0].selectionSet.selections[0].selectionSet.selections, querySchemaInfo.pathName, querySchemaInfo.returnType); if (withStatements[0].content.slice(-2) == ', ') withStatements[0].content = withStatements[0].content.substring(0, withStatements[0].content.length - 2); withStatements[0].content += '}'; returnString.push(withStatements[0].content); if (querySchemaInfo.returnIsArray) { returnString.push(')'); if (querySchemaInfo.argOptionsLimit != null) //returnString.push(` LIMIT ${querySchemaInfo.argOptionsLimit}`); returnString.push(`[..${querySchemaInfo.argOptionsLimit}]`); } else { returnString.push(' LIMIT 1'); } return finalizeGraphQuery(matchStatements, withStatements, returnString); } function transformFunctionInputParameters(fields, schemaInfo) { let r = { fields:'', graphIdValue: null }; schemaInfo.args.forEach(arg => { fields.forEach(field => { if (field.name.value === arg.name) { let value = field.value.value; if (arg.name === schemaInfo.graphDBIdArgName) { r.graphIdValue = value } else if (arg.alias != null) { let param = schemaInfo.pathName + '_' + arg.alias; r.fields += `${arg.alias}: $${param}, `; Object.assign(parameters, { [param]: value }); } else { let param = schemaInfo.pathName + '_' + arg.name; r.fields += `${arg.name}: $${param}, `; Object.assign(parameters, { [param]: value }); } } }); }); r.fields = r.fields.substring(0, r.fields.length - 2); return r; } function returnStringOnly(selections, querySchemaInfo) { withStatements.push({carryOver: querySchemaInfo.pathName, inLevel:'', content:''}); selectionsRecurse(selections, querySchemaInfo.pathName, querySchemaInfo.returnType); return `{${withStatements[0].content}}` } function resolveGrapgDBqueryForGraphQLMutation (obj, querySchemaInfo) { // createNode if (querySchemaInfo.name.startsWith('createNode') && querySchemaInfo.graphQuery == null) { const inputFields = transformFunctionInputParameters(obj.definitions[0].selectionSet.selections[0].arguments[0].value.fields, querySchemaInfo); const nodeName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; let returnBlock = `ID(${nodeName})`; if (obj.definitions[0].selectionSet.selections[0].selectionSet != undefined) { returnBlock = returnStringOnly(obj.definitions[0].selectionSet.selections[0].selectionSet.selections, querySchemaInfo); } const ocQuery = `CREATE (${nodeName}:\`${querySchemaInfo.returnTypeAlias}\` {${inputFields.fields}})\nRETURN ${returnBlock}`; return ocQuery; } // updateNode if (querySchemaInfo.name.startsWith('updateNode') && querySchemaInfo.graphQuery == null) { const inputFields = transformFunctionInputParameters(obj.definitions[0].selectionSet.selections[0].arguments[0].value.fields, querySchemaInfo); const nodeID = inputFields.graphIdValue; const nodeName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; let returnBlock = `ID(${nodeName})`; if (obj.definitions[0].selectionSet.selections[0].selectionSet != undefined) { returnBlock = returnStringOnly(obj.definitions[0].selectionSet.selections[0].selectionSet.selections, querySchemaInfo); } // :( SET += is not working, so let's work around it. //let ocQuery = `MATCH (${nodeName}) WHERE ID(${nodeName}) = '${nodeID}' SET ${nodeName} += {${inputFields}} RETURN ${returnBlock}`; // workaround: const propertyList = inputFields.fields.split(', '); let setString = ''; propertyList.forEach(property => { let kv = property.split(': '); setString = setString + ` ${nodeName}.${kv[0]} = ${kv[1]},`; }); setString = setString.substring(0, setString.length - 1); let param = nodeName + '_' + 'whereId'; Object.assign(parameters, {[param]: nodeID}); const ocQuery = `MATCH (${nodeName})\nWHERE ID(${nodeName}) = $${param}\nSET ${setString}\nRETURN ${returnBlock}`; return ocQuery; } // deleteNode if (querySchemaInfo.name.startsWith('deleteNode') && querySchemaInfo.graphQuery == null) { const nodeID = obj.definitions[0].selectionSet.selections[0].arguments[0].value.value; const nodeName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; let param = nodeName + '_' + 'whereId'; Object.assign(parameters, {[param]: nodeID}); const ocQuery = `MATCH (${nodeName})\nWHERE ID(${nodeName}) = $${param}\nDETACH DELETE ${nodeName}\nRETURN true`; return ocQuery; } // connect if (querySchemaInfo.name.startsWith('connectNode') && querySchemaInfo.graphQuery == null) { let fromID = obj.definitions[0].selectionSet.selections[0].arguments[0].value.value; let toID = obj.definitions[0].selectionSet.selections[0].arguments[1].value.value; const edgeType = querySchemaInfo.name.match(new RegExp('Edge' + "(.*)" + ''))[1]; const edgeName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; const egdgeTypeAlias = getTypeAlias(edgeType); const returnBlock = returnStringOnly(obj.definitions[0].selectionSet.selections[0].selectionSet.selections, querySchemaInfo); let paramFromId = edgeName + '_' + 'whereFromId'; let paramToId = edgeName + '_' + 'whereToId'; Object.assign(parameters, {[paramFromId]: fromID}); Object.assign(parameters, {[paramToId]: toID}); if (obj.definitions[0].selectionSet.selections[0].arguments.length > 2) { const inputFields = transformFunctionInputParameters(obj.definitions[0].selectionSet.selections[0].arguments[2].value.fields, querySchemaInfo); const ocQuery = `MATCH (from), (to)\nWHERE ID(from) = $${paramFromId} AND ID(to) = $${paramToId}\nCREATE (from)-[${edgeName}:\`${egdgeTypeAlias}\`{${inputFields.fields}}]->(to)\nRETURN ${returnBlock}`; return ocQuery; } else { const ocQuery = `MATCH (from), (to)\nWHERE ID(from) = $${paramFromId} AND ID(to) = $${paramToId}\nCREATE (from)-[${edgeName}:\`${egdgeTypeAlias}\`]->(to)\nRETURN ${returnBlock}`; return ocQuery; } } // updateEdge if (querySchemaInfo.name.startsWith('updateEdge') && querySchemaInfo.graphQuery == null) { let fromID = obj.definitions[0].selectionSet.selections[0].arguments[0].value.value; let toID = obj.definitions[0].selectionSet.selections[0].arguments[1].value.value; let edgeType = querySchemaInfo.name.match(new RegExp('updateEdge' + "(.*)" + 'From'))[1]; let egdgeTypeAlias = getTypeAlias(edgeType); const inputFields = transformFunctionInputParameters(obj.definitions[0].selectionSet.selections[0].arguments[2].value.fields, querySchemaInfo); const edgeName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; let returnBlock = `ID(${edgeName})`; if (obj.definitions[0].selectionSet.selections[0].selectionSet != undefined) { returnBlock = returnStringOnly(obj.definitions[0].selectionSet.selections[0].selectionSet.selections, querySchemaInfo); } const propertyList = inputFields.fields.split(', '); let setString = ''; propertyList.forEach(property => { let kv = property.split(': '); setString = setString + ` ${edgeName}.${kv[0]} = ${kv[1]},`; }); setString = setString.substring(0, setString.length - 1); const paramFromId = edgeName + '_' + 'whereFromId'; const paramToId = edgeName + '_' + 'whereToId'; Object.assign(parameters, {[paramFromId]: fromID}); Object.assign(parameters, {[paramToId]: toID}); const ocQuery = `MATCH (from)-[${edgeName}:$\`${egdgeTypeAlias}\`]->(to)\nWHERE ID(from) = $${paramFromId} AND ID(to) = $${paramToId}\nSET ${setString}\nRETURN ${returnBlock}`; return ocQuery; } // deleteEdge if (querySchemaInfo.name.startsWith('deleteEdge') && querySchemaInfo.graphQuery == null) { let fromID = obj.definitions[0].selectionSet.selections[0].arguments[0].value.value; let toID = obj.definitions[0].selectionSet.selections[0].arguments[1].value.value; const edgeName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; const paramFromId = edgeName + '_' + 'whereFromId'; const paramToId = edgeName + '_' + 'whereToId'; Object.assign(parameters, {[paramFromId]: fromID}); Object.assign(parameters, {[paramToId]: toID}); const ocQuery = `MATCH (from)-[${edgeName}]->(to)\nWHERE ID(from) = $${paramFromId} AND ID(to) = $${paramToId}\nDELETE ${edgeName}\nRETURN true`; return ocQuery; } // graph query directive if (querySchemaInfo.graphQuery != null) { let ocQuery = querySchemaInfo.graphQuery; if (ocQuery.includes('$input')) { const inputFields = transformFunctionInputParameters(obj.definitions[0].selectionSet.selections[0].arguments[0].value.fields, querySchemaInfo); ocQuery = ocQuery.replace('$input', inputFields.fields); } else { obj.definitions[0].selectionSet.selections[0].arguments.forEach(arg => { ocQuery = ocQuery.replace('$' + arg.name.value, arg.value.value); }); } if (ocQuery.includes('RETURN')) { const statements = ocQuery.split(' RETURN '); const entityName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; const body = statements[0].replace("this", entityName); const returnBlock = returnStringOnly(obj.definitions[0].selectionSet.selections[0].selectionSet.selections, querySchemaInfo); ocQuery = body + '\nRETURN ' + returnBlock; } return ocQuery; } return ''; } function resolveOpenCypherQuery(obj, querySchemaInfo) { let ocQuery = ''; // clear matchStatements.splice(0,matchStatements.length); withStatements.splice(0,withStatements.length); returnString.splice(0, returnString.length); parameters = {}; if (querySchemaInfo.type === 'Query') { ocQuery = resolveGrapgDBqueryForGraphQLQuery(obj, querySchemaInfo); } if (querySchemaInfo.type === 'Mutation') { ocQuery = resolveGrapgDBqueryForGraphQLMutation(obj, querySchemaInfo); } return ocQuery; } function gremlinElementToJson(o, fieldsAlias) { let data = ''; let isKey = true; data += '{'; o['@value'].forEach(v => { if (v['@value'] != undefined) { if (v['@value'] == 'label') data += '"type":'; if (v['@value'] == 'id') //data += '"id":'; data += '"' + fieldsAlias["id"] + '":'; if (v['@type'] == 'g:Int32' || v['@type'] == 'g:Double' || v['@type'] == 'g:Int64') data += v['@value'] + ', '; isKey = !isKey; } else { if (isKey) { data += '"' + fieldsAlias[v] + '":'; isKey = false; } else { data += '"' + v + '", '; isKey = true; } } }); data = data.substring(0, data.length - 2); data += '}'; return data; } function refactorGremlinqueryOutput(queryResult, fieldsAlias) { //const r = JSON.parse(queryResult).result.data; const r = queryResult; let data = ''; let isScalar = false; let isOneElement = false; let isArray = false; if (r['@value'].length == 1) { if (r['@value'][0]['@type'] == 'g:Map') isOneElement = true; else if (r['@value'][0]['@type'] == 'g:List') isArray = true; else isScalar = true } if (isScalar) { data = r['@value'][0]['@value']; } else if (isOneElement) { data += gremlinElementToJson(r['@value'][0], fieldsAlias); } else { data += '['; r['@value'][0]['@value'].forEach(e => { try { data += gremlinElementToJson(e, fieldsAlias); data +=',\n'; } catch {} }); data = data.substring(0, data.length - 2); data += ']'; } return data; } function getFieldsAlias(typeName) { const r = {}; schemaDataModel.definitions.forEach(def => { if (def.kind === 'ObjectTypeDefinition') { if (def.name.value === typeName) { def.fields.forEach(field => { let alias = field.name.value; if (field.directives.length > 0) { field.directives.forEach(directive => { if (directive.name.value === 'alias') { alias = directive.arguments[0].value.value; } if (directive.name.value === 'id') { alias = 'id'; } }); } r[alias] = field.name.value; }); } } }); return r; } function resolveGremlinQuery(obj, querySchemaInfo) { let gremlinQuery = { query:'', language: 'gremlin', parameters: {}, refactorOutput: null, fieldsAlias: getFieldsAlias(querySchemaInfo.returnType) }; // replace values from input parameters gremlinQuery.query = querySchemaInfo.graphQuery; obj.definitions[0].selectionSet.selections[0].arguments.forEach(arg => { gremlinQuery.query = gremlinQuery.query.replace('$' + arg.name.value, arg.value.value); }); return gremlinQuery; } // Function takes the graphql query and output the graphDB query function resolveGraphDBQuery(query) { let executeQuery = { query:'', parameters: {}, language: 'opencypher', refactorOutput: null }; // create a gql object from the query, gql is GraphQL Query Language const obj = gql` ${query} `; const querySchemaInfo = getSchemaQueryInfo(obj.definitions[0].selectionSet.selections[0].name.value); if (querySchemaInfo.graphQuery != null) { if (querySchemaInfo.graphQuery.startsWith('g.V')) { executeQuery.language = 'gremlin' } } if (executeQuery.language == 'opencypher') { executeQuery.query = resolveOpenCypherQuery(obj, querySchemaInfo); executeQuery.parameters = parameters; } if (executeQuery.language == 'gremlin') { executeQuery = resolveGremlinQuery(obj, querySchemaInfo); } return executeQuery; } module.exports = { resolveGraphDBQueryFromAppSyncEvent, resolveGraphDBQueryFromApolloQueryEvent, resolveGraphDBQuery, refactorGremlinqueryOutput };