templates/JSResolverOCHTTPS.js (846 lines of code) (raw):

/* 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. */ import { astFromValue, buildASTSchema, typeFromAST, GraphQLID, GraphQLInputObjectType } from 'graphql'; import { gql } from 'graphql-tag'; // GraphQL library to parse the GraphQL query const useCallSubquery = false; let schemaDataModel; let schema; /** * Initializes the schema from a given file path * * @param {object} schemaModel the path to the JSON schema data mode */ export function initSchema(schemaModel) { schemaDataModel = schemaModel; schema = buildASTSchema(schemaDataModel, { assumeValidSDL: true }); } /** * Resolves a graph db query from a given App Sync graphQL query event. * * @param {string} event.field the graphQL field being queried * @param {object} event.arguments arguments that were passed into the query * @param {string} event.selectionSetGraphQL string representation of the graphQL selection set, formatted as GraphQL schema definition language (SDL) * @returns {string} the resolved graph db query */ export function resolveGraphDBQueryFromAppSyncEvent(event) { return resolveGraphDBQueryFromEvent({ field: event.field, arguments: event.arguments, selectionSet: event.selectionSetGraphQL ? gql`${event.selectionSetGraphQL}`.definitions[0].selectionSet : {} }); } /** * Resolves a graph db query from a given graphQL query event. * * @param {string} event.field the graphQL field being queried * @param {object} event.arguments arguments that were passed into the query * @param {object} event.selectionSet the graphQL AST selection set * @returns {string} the resolved graph db query */ export function resolveGraphDBQueryFromEvent(event) { const fieldDef = getFieldDef(event.field); const args = []; for (const inputDef of fieldDef.arguments ?? []) { const value = event.arguments[inputDef.name.value]; if (value) { const inputType = typeFromAST(schema, inputDef.type); const astValue = astFromValue(value, inputType); if (inputType instanceof GraphQLInputObjectType) { // retrieve an ID field which may not necessarily be named 'id' const idField = Object.values(inputType.getFields()).find(field => field.type.name === GraphQLID.name); if (idField) { // check if id was an input arg const idValue = astValue.fields.find(f => f.name.value === idField.name); if (idValue?.value?.kind === 'IntValue') { // graphql astFromValue function can convert ID integer strings into integer type // if input args contain an id and the graphql library has interpreted the value as an int, change it to treat the value as a string idValue.value.kind = 'StringValue'; } } } args.push({ kind: 'Argument', name: { kind: 'Name', value: inputDef.name.value }, value: astValue }); } } const fieldNode = { kind: 'Field', name: { kind: 'Name', value: event.field }, arguments: args, selectionSet: event.selectionSet }; const obj = { kind: 'Document', definitions: [ { kind: 'OperationDefinition', operation: 'query', selectionSet: { kind: 'SelectionSet', selections: [fieldNode] } } ] }; const graphQuery = resolveGraphDBQuery(obj); return graphQuery; } const matchStatements = []; // openCypher match statements const withStatements = []; // openCypher with statements const returnString = []; // openCypher return statements let parameters = {}; // openCypher query parameters function getRootTypeDefs() { return getTypeDefs(['Query', 'Mutation']); } function getTypeDefs(typeNameOrNames) { if (!Array.isArray(typeNameOrNames)) { typeNameOrNames = [typeNameOrNames]; } return schemaDataModel.definitions.filter( def => def.kind === 'ObjectTypeDefinition' && typeNameOrNames.includes(def.name.value) ); } function getFieldDef(fieldName) { const rootTypeDefs = getRootTypeDefs(); for (const rootDef of rootTypeDefs) { const fieldDef = rootDef.fields.find(def => def.name.value === fieldName); if (fieldDef) { return fieldDef; } } } 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 (field.value.kind === 'IntValue' || field.value.kind === 'FloatValue') { value = Number(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; } export 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 parseQueryInput(queryObjOrStr) { // Backwards compatibility if (typeof queryObjOrStr === 'string') { return gql(queryObjOrStr); } // Already in AST format return queryObjOrStr; } /** * Accepts a GraphQL document or query string and outputs the graphDB query. * * @param {(Object|string)} queryObjOrStr the GraphQL document containing an operation to resolve * @returns {string} */ export function resolveGraphDBQuery(queryObjOrStr) { let executeQuery = { query:'', parameters: {}, language: 'opencypher', refactorOutput: null }; const obj = parseQueryInput(queryObjOrStr); 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; }