From b5457cf2d41e9d33c13aeb6651dcabfc74ffc077 Mon Sep 17 00:00:00 2001 From: Andrea Nassisi Date: Tue, 21 May 2024 22:38:46 +0000 Subject: [PATCH 01/11] Neptune Analytics - Logger --- package.json | 2 +- src/NeptuneSchema.js | 22 +++-- src/graphdb.js | 16 +++- src/main.js | 25 ++++-- src/pipelineResources.js | 109 ++++++++++++++----------- templates/Lambda4AppSyncHTTP/index.mjs | 2 +- 6 files changed, 109 insertions(+), 67 deletions(-) diff --git a/package.json b/package.json index e22a98c..7c89ea1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aws/neptune-for-graphql", - "version": "1.0.0", + "version": "1.1.0", "description": "CLI utility to create and maintain a GraphQL API for Amazon Neptune", "keywords": [ "Amazon Neptune", diff --git a/src/NeptuneSchema.js b/src/NeptuneSchema.js index e139e0e..ddd8a96 100644 --- a/src/NeptuneSchema.js +++ b/src/NeptuneSchema.js @@ -19,7 +19,8 @@ let HOST = ''; let PORT = 8182; let REGION = '' let SAMPLE = 5000; -let VERBOSE = false; +let VERBOSE = false; +let NEPTUNE_TYPE = 'neptune-db'; let language = 'openCypher'; let useSDK = false; @@ -31,7 +32,7 @@ async function getAWSCredentials() { const interceptor = aws4Interceptor({ options: { region: REGION, - service: "neptune-db", + service: NEPTUNE_TYPE, }, credentials: cred }); @@ -68,10 +69,16 @@ async function queryNeptune(q) { return response.data; } catch (error) { console.error("Http query request failed: ", error.message); - consoleOut("Trying with the AWS SDK"); - const response = await queryNeptuneSDK(q); - useSDK = true; - return response; + consoleOut("Trying with the AWS SDK"); + + if (NEPTUNE_TYPE == 'neptune-db') { + consoleOut("Trying with the AWS SDK"); + const response = await queryNeptuneSDK(q); + useSDK = true; + return response; + } + + throw new Error('AWS SDK for Neptune Analytics is not available, yet.'); } } } @@ -277,11 +284,12 @@ async function getEdgesDirectionsCardinality() { } -function setGetNeptuneSchemaParameters(host, port, region, verbose = false) { +function setGetNeptuneSchemaParameters(host, port, region, verbose = false, neptuneType) { HOST = host; PORT = port; REGION = region; VERBOSE = verbose; + NEPTUNE_TYPE = neptuneType; } diff --git a/src/graphdb.js b/src/graphdb.js index 17f43ea..9065db1 100644 --- a/src/graphdb.js +++ b/src/graphdb.js @@ -69,12 +69,16 @@ function graphDBInferenceSchema (graphbSchema, addMutations) { } r += '\t_id: ID! @id\n'; - + + let properties = []; node.properties.forEach(property => { + properties.push(property.name); + if (property.name == 'id') r+= `\tid: ID\n`; else r+= `\t${property.name}: ${property.type}\n`; + }); let edgeTypes = []; @@ -127,11 +131,15 @@ function graphDBInferenceSchema (graphbSchema, addMutations) { }); // Add edge types - edgeTypes.forEach(edgeType => { + edgeTypes.forEach(edgeType => { + let collision = ''; + if (properties.includes(edgeType)) + collision = '_'; + if (changeCase) { - r += `\t${edgeType}:${toPascalCase(edgeType)}` + r += `\t${collision + edgeType}:${toPascalCase(edgeType)}` } else { - r += `\t${edgeType}:${edgeType}` + r += `\t${collision + edgeType}:${edgeType}` } }); diff --git a/src/main.js b/src/main.js index 346d5a3..c79a804 100644 --- a/src/main.js +++ b/src/main.js @@ -52,7 +52,7 @@ let isNeptuneIAMAuth = false; let createUpdatePipeline = false; let createUpdatePipelineName = ''; let createUpdatePipelineEndpoint = ''; -let createUpdatePipelineRegion = 'us-east-1'; +let createUpdatePipelineRegion = ''; let createUpdatePipelineNeptuneDatabaseName = ''; let removePipelineName = ''; let inputCDKpipeline = false; @@ -63,6 +63,7 @@ let inputCDKpipelineRegion = ''; let inputCDKpipelineDatabaseName = ''; let createLambdaZip = true; let outputFolderPath = './output'; +let neptuneType = 'neptune-db'; // or neptune-graph // Outputs @@ -269,6 +270,12 @@ async function main() { } } + // Check if Neptune target is db or graph + if ( inputGraphDBSchemaNeptuneEndpoint.includes('neptune-graph') || + createUpdatePipelineEndpoint.includes('neptune-graph') || + inputCDKpipelineEnpoint.includes('neptune-graph')) + neptuneType = 'neptune-graph'; + // Get Neptune schema from endpoint if (inputGraphDBSchemaNeptuneEndpoint != '' && inputGraphDBSchema == '' && inputGraphDBSchemaFile == '') { let endpointParts = inputGraphDBSchemaNeptuneEndpoint.split(':'); @@ -279,8 +286,12 @@ async function main() { let neptuneHost = endpointParts[0]; let neptunePort = endpointParts[1]; - let neptuneRegionParts = inputGraphDBSchemaNeptuneEndpoint.split('.'); - let neptuneRegion = neptuneRegionParts[2]; + let neptuneRegionParts = inputGraphDBSchemaNeptuneEndpoint.split('.'); + let neptuneRegion = ''; + if (neptuneType == 'neptune-db') + neptuneRegion = neptuneRegionParts[2]; + else + neptuneRegion = neptuneRegionParts[1]; if (!quiet) console.log('Getting Neptune schema from endpoint: ' + yellow(neptuneHost + ':' + neptunePort)); setGetNeptuneSchemaParameters(neptuneHost, neptunePort, neptuneRegion, true); @@ -341,7 +352,7 @@ async function main() { console.error('AWS pipeline: a Neptune database region is required.'); process.exit(1); } - if (createUpdatePipelineEndpoint != '') { + if (createUpdatePipelineEndpoint != '' && createUpdatePipelineRegion == '') { let parts = createUpdatePipelineEndpoint.split('.'); createUpdatePipelineNeptuneDatabaseName = parts[0]; createUpdatePipelineRegion = parts[2]; @@ -548,7 +559,8 @@ async function main() { isNeptuneIAMAuth, neptuneHost, neptunePort, - outputFolderPath ); + outputFolderPath, + neptuneType ); } catch (err) { console.error('Error creating AWS pipeline: ' + err); } @@ -584,7 +596,8 @@ async function main() { isNeptuneIAMAuth, neptuneHost, neptunePort, - outputFolderPath ); + outputFolderPath, + neptuneType); } catch (err) { console.error('Error creating CDK File: ' + err); } diff --git a/src/pipelineResources.js b/src/pipelineResources.js index 9e28b30..27061c9 100644 --- a/src/pipelineResources.js +++ b/src/pipelineResources.js @@ -72,7 +72,7 @@ let NEPTUNE_CURRENT_IAM = false; let NEPTUNE_IAM_POLICY_RESOURCE = '*'; let LAMBDA_ROLE = ''; let LAMBDA_ARN = ''; -//let APPSYNC_API_ID = ''; +let NEPTUNE_TYPE = 'neptune-db'; let ZIP = null; let RESOURCES = {}; let RESOURCES_FILE = ''; @@ -98,8 +98,7 @@ async function checkPipeline() { if (!quiet) spinner = ora('Checking pipeline resources...').start(); try { - const command = new GetFunctionCommand({FunctionName: NAME +'LambdaFunction'}); - //const response = await lambdaClient.send(command); + const command = new GetFunctionCommand({FunctionName: NAME +'LambdaFunction'}); await lambdaClient.send(command); lambdaExists = true; } catch (error) { @@ -170,31 +169,35 @@ async function getNeptuneClusterinfoBy(name, region) { } -async function getNeptuneClusterinfo() { - const neptuneClient = new NeptuneClient({region: REGION}); +async function getNeptuneClusterinfo() { + if (NEPTUNE_TYPE == 'neptune-db') { + const neptuneClient = new NeptuneClient({region: REGION}); - const params = { - DBClusterIdentifier: NEPTUNE_DB_NAME - }; + const params = { + DBClusterIdentifier: NEPTUNE_DB_NAME + }; - const data = await neptuneClient.send(new DescribeDBClustersCommand(params)); - - const input = { // DescribeDBSubnetGroupsMessage - DBSubnetGroupName: data.DBClusters[0].DBSubnetGroup, - }; - const command = new DescribeDBSubnetGroupsCommand(input); - const response = await neptuneClient.send(command); + const data = await neptuneClient.send(new DescribeDBClustersCommand(params)); - NEPTUNE_HOST = data.DBClusters[0].Endpoint; - NEPTUNE_PORT = data.DBClusters[0].Port.toString(); - NEPTUNE_DBSubnetGroup = data.DBClusters[0].DBSubnetGroup; - NEPTUNE_VpcSecurityGroupId = data.DBClusters[0].VpcSecurityGroups[0].VpcSecurityGroupId; - NEPTUNE_CURRENT_IAM = data.DBClusters[0].IAMDatabaseAuthenticationEnabled; - NEPTUNE_CURRENT_VERSION = data.DBClusters[0].EngineVersion; - NEPTUNE_IAM_POLICY_RESOURCE = `${data.DBClusters[0].DBClusterArn.substring(0, data.DBClusters[0].DBClusterArn.lastIndexOf(':cluster')).replace('rds', 'neptune-db')}:${data.DBClusters[0].DbClusterResourceId}/*`; - response.DBSubnetGroups[0].Subnets.forEach(element => { - NEPTUNE_DBSubnetIds.push(element.SubnetIdentifier); - }); + const input = { // DescribeDBSubnetGroupsMessage + DBSubnetGroupName: data.DBClusters[0].DBSubnetGroup, + }; + const command = new DescribeDBSubnetGroupsCommand(input); + const response = await neptuneClient.send(command); + + NEPTUNE_HOST = data.DBClusters[0].Endpoint; + NEPTUNE_PORT = data.DBClusters[0].Port.toString(); + NEPTUNE_DBSubnetGroup = data.DBClusters[0].DBSubnetGroup; + NEPTUNE_VpcSecurityGroupId = data.DBClusters[0].VpcSecurityGroups[0].VpcSecurityGroupId; + NEPTUNE_CURRENT_IAM = data.DBClusters[0].IAMDatabaseAuthenticationEnabled; + NEPTUNE_CURRENT_VERSION = data.DBClusters[0].EngineVersion; + NEPTUNE_IAM_POLICY_RESOURCE = `${data.DBClusters[0].DBClusterArn.substring(0, data.DBClusters[0].DBClusterArn.lastIndexOf(':cluster')).replace('rds', 'neptune-db')}:${data.DBClusters[0].DbClusterResourceId}/*`; + response.DBSubnetGroups[0].Subnets.forEach(element => { + NEPTUNE_DBSubnetIds.push(element.SubnetIdentifier); + }); + } else { + throw new Error('AWS SDK for Neptune Analytics is not available, yet.'); + } } @@ -221,7 +224,7 @@ async function createLambdaRole() { await sleep(10000); LAMBDA_ROLE = data.Role.Arn; storeResource({LambdaExecutionRole: NAME +"LambdaExecutionRole"}); - if (!quiet) spinner.succeed('Role ARN: ' + yellow(LAMBDA_ROLE)); + if (!quiet) spinner.succeed('Role ARN: ' + yellow(LAMBDA_ROLE)); // Attach to Lambda role the AWSLambdaBasicExecutionRole if (!quiet) spinner = ora('Attaching policies to the Lambda principal role ...').start(); @@ -236,6 +239,19 @@ async function createLambdaRole() { if (NEPTUME_IAM_AUTH) { + + let action = []; + if (NEPTUNE_TYPE == 'neptune-db') { + action = [ + "neptune-db:DeleteDataViaQuery", + "neptune-db:connect", + "neptune-db:ReadDataViaQuery", + "neptune-db:WriteDataViaQuery" + ]; + } else { + action = "neptune-graph:*" + } + // Create Neptune query policy if (!quiet) spinner = ora('Creating policy for Neptune queries ...').start(); let command = new CreatePolicyCommand({ @@ -244,12 +260,7 @@ async function createLambdaRole() { Statement: [ { Effect: "Allow", - Action: [ - "neptune-db:DeleteDataViaQuery", - "neptune-db:connect", - "neptune-db:ReadDataViaQuery", - "neptune-db:WriteDataViaQuery" - ], + Action: action, Resource: NEPTUNE_IAM_POLICY_RESOURCE }, ], @@ -330,7 +341,8 @@ async function createLambdaFunction() { "NEPTUNE_HOST": NEPTUNE_HOST, "NEPTUNE_PORT": NEPTUNE_PORT, "NEPTUNE_IAM_AUTH_ENABLED": "true", - "LOGGING_ENABLED": "false" + "LOGGING_ENABLED": "false", + "NEPTUNE_TYPE": NEPTUNE_TYPE }, }, }; @@ -365,7 +377,7 @@ async function createLambdaFunction() { //await sleep(5000); LAMBDA_ARN = data.FunctionArn; storeResource({LambdaFunction: NAME +'LambdaFunction'}); - if (!quiet) spinner.succeed('Lambda Name: ' + yellow(NAME +'LambdaFunction') + ' ARN: ' + yellow(LAMBDA_ARN)); + if (!quiet) spinner.succeed('Lambda Name: ' + yellow(NAME +'LambdaFunction') + ' ARN: ' + yellow(LAMBDA_ARN)); } @@ -628,7 +640,7 @@ export function response(ctx) { const command = new CreateResolverCommand(input); await client.send(command); await sleep(200); - if (!quiet) spinner.succeed('Attached resolver to schema type ' + yellow(typeName) + ' field ' + yellow(fieldName)); + if (!quiet) spinner.succeed('Attached resolver to schema type ' + yellow(typeName) + ' field ' + yellow(fieldName)); } @@ -648,7 +660,7 @@ async function removeAWSpipelineResources(resources, quietI) { await appSyncClient.send(command); if (!quiet) spinner.succeed('Deleted API id: ' + yellow(resources.AppSyncAPI)); } catch (error) { - if (!quiet) spinner.fail('AppSync API delete failed: ' + error); + if (!quiet) spinner.fail('AppSync API delete failed: ' + error); } // Lambda @@ -661,7 +673,7 @@ async function removeAWSpipelineResources(resources, quietI) { await lambdaClient.send(command); if (!quiet) spinner.succeed('Lambda function deleted: ' + yellow(resources.LambdaFunction)); } catch (error) { - if (!quiet) spinner.fail('Lambda function fail to delete: ' + error); + if (!quiet) spinner.fail('Lambda function fail to delete: ' + error); } // Lambda execution role @@ -675,7 +687,7 @@ async function removeAWSpipelineResources(resources, quietI) { await iamClient.send(command); if (!quiet) spinner.succeed('Detached policy: ' + yellow(resources.LambdaExecutionPolicy1) + " from role: " + yellow(resources.LambdaExecutionRole)); } catch (error) { - if (!quiet) spinner.fail('Detach policy failed: ' + error); + if (!quiet) spinner.fail('Detach policy failed: ' + error); } if (!quiet) spinner = ora('Detaching IAM policies from role ...').start(); @@ -686,9 +698,9 @@ async function removeAWSpipelineResources(resources, quietI) { }; let command = new DetachRolePolicyCommand(input); await iamClient.send(command); - if (!quiet) spinner.succeed('Detached policy: ' + yellow(resources.LambdaExecutionPolicy1) + " from role: " + yellow(resources.LambdaExecutionRole)); + if (!quiet) spinner.succeed('Detached policy: ' + yellow(resources.LambdaExecutionPolicy2) + " from role: " + yellow(resources.LambdaExecutionRole)); } catch (error) { - if (!quiet) spinner.fail('Detach policy failed: ' + error); + if (!quiet) spinner.fail('Detach policy failed: ' + error); } // Delete Neptune query Policy @@ -702,7 +714,7 @@ async function removeAWSpipelineResources(resources, quietI) { await iamClient.send(command); if (!quiet) spinner.succeed('Deleted policy: ' + yellow(resources.NeptuneQueryPolicy)); } catch (error) { - if (!quiet) spinner.fail('Delete policy failed: ' + error); + if (!quiet) spinner.fail('Delete policy failed: ' + error); } } @@ -716,7 +728,7 @@ async function removeAWSpipelineResources(resources, quietI) { await iamClient.send(command); if (!quiet) spinner.succeed('Deleted role: ' + yellow(resources.LambdaExecutionRole)); } catch (error) { - if (!quiet) spinner.fail('Delete role failed: ' + error); + if (!quiet) spinner.fail('Delete role failed: ' + error); } // AppSync Lambda role @@ -730,7 +742,7 @@ async function removeAWSpipelineResources(resources, quietI) { await iamClient.send(command); if (!quiet) spinner.succeed('Detached policy: ' + yellow(resources.LambdaInvokePolicy) + " from role: " + yellow(resources.LambdaInvokeRole)); } catch (error) { - if (!quiet) spinner.fail('Detach policy failed: ' + error); + if (!quiet) spinner.fail('Detach policy failed: ' + error); } // Delete Policy @@ -743,7 +755,7 @@ async function removeAWSpipelineResources(resources, quietI) { await iamClient.send(command); if (!quiet) spinner.succeed('Deleted policy: ' + yellow(resources.LambdaInvokePolicy)); } catch (error) { - if (!quiet) spinner.fail('Delete policy failed: ' + error); + if (!quiet) spinner.fail('Delete policy failed: ' + error); } // Delete Role @@ -756,7 +768,7 @@ async function removeAWSpipelineResources(resources, quietI) { await iamClient.send(command); if (!quiet) spinner.succeed('Deleted role: ' + yellow(resources.LambdaInvokeRole)); } catch (error) { - if (!quiet) spinner.fail('Delete role failed: ' + error); + if (!quiet) spinner.fail('Delete role failed: ' + error); } } @@ -794,7 +806,7 @@ async function updateAppSyncAPI(resources) { } -async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBregion, appSyncSchema, schemaModel, lambdaFilesPath, addMutations, quietI, __dirname, isNeptuneIAMAuth, neptuneHost, neptunePort, outputFolderPath) { +async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBregion, appSyncSchema, schemaModel, lambdaFilesPath, addMutations, quietI, __dirname, isNeptuneIAMAuth, neptuneHost, neptunePort, outputFolderPath, neptuneType) { NAME = pipelineName; REGION = neptuneDBregion; @@ -809,8 +821,9 @@ async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBre NEPTUNE_HOST = neptuneHost; NEPTUNE_PORT = neptunePort; thisOutputFolderPath = outputFolderPath; + NEPTUNE_TYPE = neptuneType; - if (!quiet) console.log('\nCheck if the pipeline resources have been created'); + if (!quiet) console.log('\nCheck if the pipeline resources have been created'); await checkPipeline(); if (!pipelineExists) { @@ -873,7 +886,7 @@ async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBre if (!quiet) console.log('Create AppSync API'); await createAppSyncAPI(); - if (!quiet) console.log('Saved resorces to file: ' + yellow(RESOURCES_FILE)); + if (!quiet) console.log('Saved resorces to file: ' + yellow(RESOURCES_FILE)); } catch (error) { if (!quiet) spinner.fail('Error creating resources: ' + error); diff --git a/templates/Lambda4AppSyncHTTP/index.mjs b/templates/Lambda4AppSyncHTTP/index.mjs index ed22a69..06f6569 100644 --- a/templates/Lambda4AppSyncHTTP/index.mjs +++ b/templates/Lambda4AppSyncHTTP/index.mjs @@ -17,7 +17,7 @@ if (process.env.NEPTUNE_IAM_AUTH_ENABLED === 'true') { const interceptor = aws4Interceptor({ options: { region: AWS_REGION, - service: "neptune-db", + service: process.env.NEPTUNE_TYPE, }, credentials: { accessKeyId: AWS_ACCESS_KEY_ID, From 3918f559e5b51dfadd43942f41ed3a2d7e7ffb2b Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Wed, 18 Sep 2024 12:22:47 -0700 Subject: [PATCH 02/11] Skipped retrieving neptune cluster info for analytics. --- src/pipelineResources.js | 79 ++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/src/pipelineResources.js b/src/pipelineResources.js index 27061c9..cde627c 100644 --- a/src/pipelineResources.js +++ b/src/pipelineResources.js @@ -223,7 +223,7 @@ async function createLambdaRole() { //await waitUntilRoleExists({ client: iamClient, maxWaitTime: 180 }, { RoleName: data.Role.RoleName }); // does not work :(, using sleep await sleep(10000); LAMBDA_ROLE = data.Role.Arn; - storeResource({LambdaExecutionRole: NAME +"LambdaExecutionRole"}); + storeResource({LambdaExecutionRole: NAME +"LambdaExecutionRole"}); if (!quiet) spinner.succeed('Role ARN: ' + yellow(LAMBDA_ROLE)); // Attach to Lambda role the AWSLambdaBasicExecutionRole @@ -659,7 +659,7 @@ async function removeAWSpipelineResources(resources, quietI) { const command = new DeleteGraphqlApiCommand(input); await appSyncClient.send(command); if (!quiet) spinner.succeed('Deleted API id: ' + yellow(resources.AppSyncAPI)); - } catch (error) { + } catch (error) { if (!quiet) spinner.fail('AppSync API delete failed: ' + error); } @@ -806,7 +806,7 @@ async function updateAppSyncAPI(resources) { } -async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBregion, appSyncSchema, schemaModel, lambdaFilesPath, addMutations, quietI, __dirname, isNeptuneIAMAuth, neptuneHost, neptunePort, outputFolderPath, neptuneType) { +async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBregion, appSyncSchema, schemaModel, lambdaFilesPath, addMutations, quietI, __dirname, isNeptuneIAMAuth, neptuneHost, neptunePort, outputFolderPath, neptuneType) { NAME = pipelineName; REGION = neptuneDBregion; @@ -829,49 +829,50 @@ async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBre if (!pipelineExists) { try { storeResource({region: REGION}); - - try { - if (!quiet) console.log('Get Neptune Cluster Info'); - if (!quiet) spinner = ora('Getting ...').start(); - await getNeptuneClusterinfo(); - if (!quiet) spinner.succeed('Got Neptune Cluster Info'); - if (isNeptuneIAMAuth) { - if (!NEPTUNE_CURRENT_IAM) { - console.error("The Neptune database authentication is set to VPC."); - console.error("Remove the --create-update-aws-pipeline-neptune-IAM option."); - exit(1); - } - } else { - if (NEPTUNE_CURRENT_IAM) { - console.error("The Neptune database authentication is set to IAM."); - console.error("Add the --create-update-aws-pipeline-neptune-IAM option."); - exit(1); + + if (NEPTUNE_TYPE === 'neptune-db') { + try { + if (!quiet) console.log('Get Neptune Cluster Info'); + if (!quiet) spinner = ora('Getting ...').start(); + await getNeptuneClusterinfo(); + if (!quiet) spinner.succeed('Got Neptune Cluster Info'); + if (isNeptuneIAMAuth) { + if (!NEPTUNE_CURRENT_IAM) { + console.error("The Neptune database authentication is set to VPC."); + console.error("Remove the --create-update-aws-pipeline-neptune-IAM option."); + exit(1); + } } else { - if (!quiet) console.log(`Subnet Group: ` + yellow(NEPTUNE_DBSubnetGroup)); + if (NEPTUNE_CURRENT_IAM) { + console.error("The Neptune database authentication is set to IAM."); + console.error("Add the --create-update-aws-pipeline-neptune-IAM option."); + exit(1); + } else { + if (!quiet) console.log(`Subnet Group: ` + yellow(NEPTUNE_DBSubnetGroup)); + } } - } - if (NEPTUNE_CURRENT_VERSION != '') { - const v = NEPTUNE_CURRENT_VERSION; - if (lambdaFilesPath.includes('SDK') == true && - (v == '1.2.1.0' || v == '1.2.0.2' || v == '1.2.0.1' || v == '1.2.0.0' || v == '1.1.1.0' || v == '1.1.0.0')) { - console.error("Neptune SDK query is supported starting with Neptune versions 1.2.2.0"); - console.error("Switch to Neptune HTTPS query with option --output-resolver-query-https"); - exit(1); + if (NEPTUNE_CURRENT_VERSION != '') { + const v = NEPTUNE_CURRENT_VERSION; + if (lambdaFilesPath.includes('SDK') == true && + (v == '1.2.1.0' || v == '1.2.0.2' || v == '1.2.0.1' || v == '1.2.0.0' || v == '1.1.1.0' || v == '1.1.0.0')) { + console.error("Neptune SDK query is supported starting with Neptune versions 1.2.2.0"); + console.error("Switch to Neptune HTTPS query with option --output-resolver-query-https"); + exit(1); + } } - } - } catch (error) { - if (!quiet) spinner.fail("Error getting Neptune Cluster Info."); - if (!isNeptuneIAMAuth) { - console.error("VPC data is not available to proceed."); - exit(1); - } else { - if (!quiet) console.log("Could not read the database ARN to restrict the Lambda permissions. \nTo increase security change the resource in the Neptune Query policy.") - if (!quiet) console.log("Proceeding without getting Neptune Cluster info."); + } catch (error) { + if (!quiet) spinner.fail("Error getting Neptune Cluster Info."); + if (!isNeptuneIAMAuth) { + console.error("VPC data is not available to proceed."); + exit(1); + } else { + if (!quiet) console.log("Could not read the database ARN to restrict the Lambda permissions. \nTo increase security change the resource in the Neptune Query policy.") + if (!quiet) console.log("Proceeding without getting Neptune Cluster info."); + } } } - if (!quiet) console.log('Create ZIP'); if (!quiet) spinner = ora('Creating ZIP ...').start(); ZIP = await createDeploymentPackage(LAMBDA_FILES_PATH) From 67d2be2341b57241251b500069b6a34b8d69cca7 Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Fri, 11 Oct 2024 22:43:38 -0700 Subject: [PATCH 03/11] Change host parsing to check for 2 parts. --- src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index c79a804..0e08c36 100644 --- a/src/main.js +++ b/src/main.js @@ -279,7 +279,7 @@ async function main() { // Get Neptune schema from endpoint if (inputGraphDBSchemaNeptuneEndpoint != '' && inputGraphDBSchema == '' && inputGraphDBSchemaFile == '') { let endpointParts = inputGraphDBSchemaNeptuneEndpoint.split(':'); - if (endpointParts.length < 2) { + if (endpointParts.length !== 2) { console.error('Neptune endpoint must be in the form of host:port'); process.exit(1); } From 93033f9797c11533b7a5eb601c804950b539b720 Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Fri, 20 Sep 2024 09:44:33 -0700 Subject: [PATCH 04/11] Removed default region and changed parsing of region from URL to differ for analytics vs neptune db. --- src/main.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main.js b/src/main.js index 0e08c36..88f0d22 100644 --- a/src/main.js +++ b/src/main.js @@ -263,7 +263,7 @@ async function main() { if (inputGraphDBSchemaFile != '' && inputGraphQLSchema == '' && inputGraphQLSchemaFile == '') { try { inputGraphDBSchema = readFileSync(inputGraphDBSchemaFile, 'utf8'); - if (!quiet) console.log('Loaded graphDB schema from file: ' + inputGraphDBSchemaFile); + if (!quiet) console.log('Loaded graphDB schema from file: ' + inputGraphDBSchemaFile); } catch (err) { console.error('Error reading graphDB schema file: ' + inputGraphDBSchemaFile); process.exit(1); @@ -355,7 +355,11 @@ async function main() { if (createUpdatePipelineEndpoint != '' && createUpdatePipelineRegion == '') { let parts = createUpdatePipelineEndpoint.split('.'); createUpdatePipelineNeptuneDatabaseName = parts[0]; - createUpdatePipelineRegion = parts[2]; + if (neptuneType === 'neptune-db') { + createUpdatePipelineRegion = parts[2]; + } else { + createUpdatePipelineRegion = parts[1]; + } } if (createUpdatePipelineName == '') { createUpdatePipelineName = createUpdatePipelineNeptuneDatabaseName; @@ -433,7 +437,7 @@ async function main() { writeFileSync(outputSchemaFile, outputSchema); if (!quiet) console.log('Wrote GraphQL schema to file: ' + yellow(outputSchemaFile)); } catch (err) { - console.error('Error writing GraphQL schema to file: ' + outputSchemaFile); + console.error('Error writing GraphQL schema to file: ' + outputSchemaFile); } @@ -451,7 +455,7 @@ async function main() { writeFileSync(outputSourceSchemaFile, outputSourceSchema); if (!quiet) console.log('Wrote GraphQL schema to file: ' + yellow(outputSourceSchemaFile)); } catch (err) { - console.error('Error writing GraphQL schema to file: ' + outputSourceSchemaFile); + console.error('Error writing GraphQL schema to file: ' + outputSourceSchemaFile); } @@ -468,7 +472,7 @@ async function main() { writeFileSync(outputNeptuneSchemaFile, inputGraphDBSchema); if (!quiet) console.log('Wrote Neptune schema to file: ' + yellow(outputNeptuneSchemaFile)); } catch (err) { - console.error('Error writing Neptune schema to file: ' + outputNeptuneSchemaFile); + console.error('Error writing Neptune schema to file: ' + outputNeptuneSchemaFile); } @@ -481,7 +485,7 @@ async function main() { writeFileSync(outputLambdaResolverFile, outputLambdaResolver); if (!quiet) console.log('Wrote Lambda resolver to file: ' + yellow(outputLambdaResolverFile)); } catch (err) { - console.error('Error writing Lambda resolver to file: ' + outputLambdaResolverFile); + console.error('Error writing Lambda resolver to file: ' + outputLambdaResolverFile); } @@ -499,7 +503,7 @@ async function main() { //writeFileSync('./test/output.resolver.graphql.js', outputJSResolver); // Remove, for development and test only if (!quiet) console.log('Wrote Javascript resolver to file: ' + yellow(outputJSResolverFile)); } catch (err) { - console.error('Error writing Javascript resolver to file: ' + outputJSResolverFile); + console.error('Error writing Javascript resolver to file: ' + outputJSResolverFile); } @@ -546,7 +550,7 @@ async function main() { let neptuneHost = endpointParts[0]; let neptunePort = endpointParts[1]; - if (!quiet) console.log('\nCreating AWS pipeline resources') + if (!quiet) console.log('\nCreating AWS pipeline resources') await createUpdateAWSpipeline( createUpdatePipelineName, createUpdatePipelineNeptuneDatabaseName, createUpdatePipelineRegion, @@ -562,7 +566,7 @@ async function main() { outputFolderPath, neptuneType ); } catch (err) { - console.error('Error creating AWS pipeline: ' + err); + console.error('Error creating AWS pipeline: ' + err); } } @@ -599,16 +603,16 @@ async function main() { outputFolderPath, neptuneType); } catch (err) { - console.error('Error creating CDK File: ' + err); + console.error('Error creating CDK File: ' + err); } } - if (!quiet) console.log('\nDone\n'); + if (!quiet) console.log('\nDone\n'); } // Remove AWS Pipeline if ( removePipelineName != '') { - if (!quiet) console.log('\nRemoving pipeline AWS resources, name: ' + yellow(removePipelineName)) + if (!quiet) console.log('\nRemoving pipeline AWS resources, name: ' + yellow(removePipelineName)) let resourcesToRemove = null; let resourcesFile = `${outputFolderPath}/${removePipelineName}-resources.json`; if (!quiet) console.log('Using file: ' + yellow(resourcesFile)); From 3cde5d51e980a6afb1e4c431e06b614830556c3b Mon Sep 17 00:00:00 2001 From: andreachild Date: Tue, 8 Oct 2024 14:59:46 -0700 Subject: [PATCH 05/11] Integrate usage of neptune graph SDK for schema queries and lambda logic (#5) Introduce usage of the neptune graph (analytics) SDK in a couple scenarios: 1. during pipeline creation, if an axios request fails when querying the Neptune Analytics graph and falls back to SDK and 2. if the user has opted to use SDK for the generated lambda (as opposed to http). Summary of changes: -added new dependency on client-neptune-graph version 3.662.0 -created new lambda template which is used if the user specifies --output-resolver-query-sdk option -set additional lambda environment variable for neptune db name which is required to execute queries using the neptune graph SDK -added logic to fall back to neptune graph SDK if Axios request fails during pipeline creation (previous logic threw Error as the analytics SDK was not yet available) -fixed function which retrieves graph summary to use neptune graph SDK if the neptune-type is neptune-graph (the summary endpoint path for neptune-db is not the same for neptune-graph) -fixed CDK pipeline to only fetch cluster info if the type is neptune-db as it is not required for Neptune-graph (analytics) -set isNeptuneIAMAuth to true if the neptune type is detected as neptune-graph -introduced util.js for parsing functions that are used across multiple modules -refactored function which had many params to use an object param instead for better readability -introduced new test case 7 which sets --output-resolver-query-sdk option --- .gitignore | 2 + package.json | 2 + src/CDKPipelineApp.js | 102 +- src/NeptuneSchema.js | 191 +- src/main.js | 81 +- src/pipelineResources.js | 186 +- src/test/util.test.js | 25 + src/util.js | 60 + templates/CDKTemplate.js | 29 +- templates/Lambda4AppSyncGraphSDK/index.mjs | 93 + templates/Lambda4AppSyncGraphSDK/package.json | 15 + test/TestCases/.DS_Store | Bin 0 -> 6148 bytes test/TestCases/Case07/.DS_Store | Bin 0 -> 6148 bytes test/TestCases/Case07/Case07.01.test.js | 13 + test/TestCases/Case07/Case07.02.test.js | 6 + test/TestCases/Case07/Case07.03.test.js | 13 + test/TestCases/Case07/case01.json | 18 + test/TestCases/Case07/case02.json | 9 + .../output.resolver.graphql.js | 4536 +++++++++++++++++ .../outputReference/output.schema.graphql | 151 + .../output.source.schema.graphql | 151 + test/package.json | 3 +- test/testLib.js | 16 +- 23 files changed, 5473 insertions(+), 229 deletions(-) create mode 100644 src/test/util.test.js create mode 100644 src/util.js create mode 100644 templates/Lambda4AppSyncGraphSDK/index.mjs create mode 100644 templates/Lambda4AppSyncGraphSDK/package.json create mode 100644 test/TestCases/.DS_Store create mode 100644 test/TestCases/Case07/.DS_Store create mode 100644 test/TestCases/Case07/Case07.01.test.js create mode 100644 test/TestCases/Case07/Case07.02.test.js create mode 100644 test/TestCases/Case07/Case07.03.test.js create mode 100644 test/TestCases/Case07/case01.json create mode 100644 test/TestCases/Case07/case02.json create mode 100644 test/TestCases/Case07/outputReference/output.resolver.graphql.js create mode 100644 test/TestCases/Case07/outputReference/output.schema.graphql create mode 100644 test/TestCases/Case07/outputReference/output.source.schema.graphql diff --git a/.gitignore b/.gitignore index 61de367..09595a7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ package-lock.json node_modules/** templates/Lambda4AppSyncHTTP/node_modules/** templates/Lambda4AppSyncSDK/node_modules/** +templates/Lambda4AppSyncGraphSDK/node_modules/** coverage/** test/node_modules/** test/TestCases/Case01/output/** @@ -13,4 +14,5 @@ test/TestCases/Case03/output/** test/TestCases/Case04/output/** test/TestCases/Case05/output/** test/TestCases/Case06/output/** +test/TestCases/Case07/output/** *.iml diff --git a/package.json b/package.json index 7c89ea1..b429066 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "postinstall": "cd templates/Lambda4AppSyncHTTP && npm install && cd ../Lambda4AppSyncSDK && npm install", "lint": "eslint neptune-for-graphql.mjs ./src", "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js", + "test:sdk": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=test/TestCases/Case07", "test:resolver": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=test/TestCases/Case01" }, "jest": { @@ -53,6 +54,7 @@ "@aws-sdk/client-lambda": "3.387.0", "@aws-sdk/client-neptune": "3.387.0", "@aws-sdk/client-neptunedata": "3.403.0", + "@aws-sdk/client-neptune-graph": "3.662.0", "@aws-sdk/credential-providers": "3.414.0", "archiver": "5.3.1", "aws4-axios": "3.3.0", diff --git a/src/CDKPipelineApp.js b/src/CDKPipelineApp.js index e7d64dd..bb4f0fe 100644 --- a/src/CDKPipelineApp.js +++ b/src/CDKPipelineApp.js @@ -10,7 +10,7 @@ express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { getNeptuneClusterinfoBy } from './pipelineResources.js' +import { getNeptuneClusterDbInfoBy } from './pipelineResources.js' import { readFile, writeFile } from 'fs/promises'; //import semver from 'semver'; import fs from 'fs'; @@ -73,7 +73,22 @@ async function createDeploymentFile(folderPath, zipFilePath) { } -async function createAWSpipelineCDK (pipelineName, neptuneDBName, neptuneDBregion, appSyncSchema, schemaModel, lambdaFilesPath, outputFile, __dirname, quiet, isNeptuneIAMAuth, neptuneHost, neptunePort, outputFolderPath ) { +async function createAWSpipelineCDK({ + pipelineName, + neptuneDBName, + neptuneDBregion, + appSyncSchema, + schemaModel, + lambdaFilesPath, + outputFile, + __dirname, + quiet, + isNeptuneIAMAuth, + neptuneHost, + neptunePort, + outputFolderPath, + neptuneType + }) { NAME = pipelineName; REGION = neptuneDBregion; @@ -88,50 +103,52 @@ async function createAWSpipelineCDK (pipelineName, neptuneDBName, neptuneDBregio let spinner = null; let neptuneClusterInfo = null; - try { - if (!quiet) console.log('Get Neptune Cluster Info'); - if (!quiet) spinner = ora('Getting ...').start(); - neptuneClusterInfo = await getNeptuneClusterinfoBy(NEPTUNE_DB_NAME, REGION); - if (!quiet) spinner.succeed('Got Neptune Cluster Info'); - if (isNeptuneIAMAuth) { - if (!neptuneClusterInfo.isIAMauth) { - console.error("The Neptune database authentication is set to VPC."); - console.error("Remove the --output-aws-pipeline-cdk-neptune-IAM option."); - process.exit(1); - } - } else { - if (neptuneClusterInfo.isIAMauth) { - console.error("The Neptune database authentication is set to IAM."); - console.error("Add the --output-aws-pipeline-cdk-neptune-IAM option."); - process.exit(1); + if (neptuneType === 'neptune-db') { + try { + if (!quiet) console.log('Get Neptune Cluster Info'); + if (!quiet) spinner = ora('Getting ...').start(); + neptuneClusterInfo = await getNeptuneClusterDbInfoBy(NEPTUNE_DB_NAME, REGION); + if (!quiet) spinner.succeed('Got Neptune Cluster Info'); + if (isNeptuneIAMAuth) { + if (!neptuneClusterInfo.isIAMauth) { + console.error("The Neptune database authentication is set to VPC."); + console.error("Remove the --output-aws-pipeline-cdk-neptune-IAM option."); + process.exit(1); + } } else { - if (!quiet) console.log(`Subnet Group: ` + yellow(neptuneClusterInfo.dbSubnetGroup)); + if (neptuneClusterInfo.isIAMauth) { + console.error("The Neptune database authentication is set to IAM."); + console.error("Add the --output-aws-pipeline-cdk-neptune-IAM option."); + process.exit(1); + } else { + if (!quiet) console.log(`Subnet Group: ` + yellow(neptuneClusterInfo.dbSubnetGroup)); + } } - } - if (neptuneClusterInfo.version != '') { - const v = neptuneClusterInfo.version; - if (lambdaFilesPath.includes('SDK') == true && //semver.satisfies(v, '>=1.2.1.0') ) { - (v == '1.2.1.0' || v == '1.2.0.2' || v == '1.2.0.1' || v == '1.2.0.0' || v == '1.1.1.0' || v == '1.1.0.0')) { - console.error("Neptune SDK query is supported starting with Neptune versions 1.2.1.0.R5"); - console.error("Switch to Neptune HTTPS query with option --output-resolver-query-https"); - process.exit(1); + if (neptuneClusterInfo.version != '') { + const v = neptuneClusterInfo.version; + if (lambdaFilesPath.includes('SDK') == true && //semver.satisfies(v, '>=1.2.1.0') ) { + (v == '1.2.1.0' || v == '1.2.0.2' || v == '1.2.0.1' || v == '1.2.0.0' || v == '1.1.1.0' || v == '1.1.0.0')) { + console.error("Neptune SDK query is supported starting with Neptune versions 1.2.1.0.R5"); + console.error("Switch to Neptune HTTPS query with option --output-resolver-query-https"); + process.exit(1); + } } - } - NEPTUNE_HOST = neptuneClusterInfo.host; - NEPTUNE_PORT = neptuneClusterInfo.port; - NEPTUNE_DBSubnetGroup = neptuneClusterInfo.dbSubnetGroup.replace('default-', ''); - NEPTUNE_IAM_POLICY_RESOURCE = neptuneClusterInfo.iamPolicyResource; - - } catch (error) { - if (!quiet) spinner.fail("Error getting Neptune Cluster Info."); - if (!isNeptuneIAMAuth) { - spinner.clear(); - console.error("VPC data is not available to proceed."); - process.exit(1); - } else { - if (!quiet) console.log("Proceeding without getting Neptune Cluster info."); + NEPTUNE_HOST = neptuneClusterInfo.host; + NEPTUNE_PORT = neptuneClusterInfo.port; + NEPTUNE_DBSubnetGroup = neptuneClusterInfo.dbSubnetGroup.replace('default-', ''); + NEPTUNE_IAM_POLICY_RESOURCE = neptuneClusterInfo.iamPolicyResource; + + } catch (error) { + if (!quiet) spinner.fail("Error getting Neptune Cluster Info."); + if (!isNeptuneIAMAuth) { + spinner.clear(); + console.error("VPC data is not available to proceed."); + process.exit(1); + } else { + if (!quiet) console.log("Proceeding without getting Neptune Cluster info."); + } } } @@ -147,7 +164,8 @@ async function createAWSpipelineCDK (pipelineName, neptuneDBName, neptuneDBregio CDKFile = CDKFile.replace( "const NAME = '';", `const NAME = '${NAME}';` ); CDKFile = CDKFile.replace( "const REGION = '';", `const REGION = '${REGION}';` ); CDKFile = CDKFile.replace( "const NEPTUNE_HOST = '';", `const NEPTUNE_HOST = '${NEPTUNE_HOST}';` ); - CDKFile = CDKFile.replace( "const NEPTUNE_PORT = '';", `const NEPTUNE_PORT = '${NEPTUNE_PORT}';` ); + CDKFile = CDKFile.replace( "const NEPTUNE_PORT = '';", `const NEPTUNE_PORT = '${NEPTUNE_PORT}';` ); + CDKFile = CDKFile.replace( "const NEPTUNE_DB_NAME = '';", `const NEPTUNE_DB_NAME = '${NEPTUNE_DB_NAME}';` ); CDKFile = CDKFile.replace( "const NEPTUNE_DBSubnetGroup = null;", `const NEPTUNE_DBSubnetGroup = '${NEPTUNE_DBSubnetGroup}';` ); CDKFile = CDKFile.replace( "const NEPTUNE_IAM_AUTH = false;", `const NEPTUNE_IAM_AUTH = ${isNeptuneIAMAuth};` ); CDKFile = CDKFile.replace( "const NEPTUNE_IAM_POLICY_RESOURCE = '*';", `const NEPTUNE_IAM_POLICY_RESOURCE = '${NEPTUNE_IAM_POLICY_RESOURCE}';` ); diff --git a/src/NeptuneSchema.js b/src/NeptuneSchema.js index ddd8a96..f7350d2 100644 --- a/src/NeptuneSchema.js +++ b/src/NeptuneSchema.js @@ -14,16 +14,23 @@ import axios from "axios"; import { aws4Interceptor } from "aws4-axios"; import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; import { NeptunedataClient, ExecuteOpenCypherQueryCommand } from "@aws-sdk/client-neptunedata"; +import { parseNeptuneDomain, parseNeptuneGraphName } from "./util.js"; +import { ExecuteQueryCommand, GetGraphSummaryCommand, NeptuneGraphClient } from "@aws-sdk/client-neptune-graph"; +const NEPTUNE_DB = 'neptune-db'; +const NEPTUNE_GRAPH_PROTOCOL = 'https'; +const HTTP_LANGUAGE = 'openCypher'; +const NEPTUNE_GRAPH_LANGUAGE = 'OPEN_CYPHER'; let HOST = ''; let PORT = 8182; let REGION = '' let SAMPLE = 5000; let VERBOSE = false; -let NEPTUNE_TYPE = 'neptune-db'; -let language = 'openCypher'; +let NEPTUNE_TYPE = NEPTUNE_DB; +let NAME = ''; let useSDK = false; - +let neptuneGraphClient; +let neptunedataClient; async function getAWSCredentials() { const credentialProvider = fromNodeProviderChain(); @@ -59,46 +66,81 @@ function consoleOut(text) { } -async function queryNeptune(q) { +/** + * Executes a neptune query using HTTP or SDK. + * @param query the query to execute + * @param params optional query params + * @returns {Promise} + */ +async function queryNeptune(query, params = '{}') { if (useSDK) { - const response = await queryNeptuneSDK(q); - return response; + return await queryNeptuneSdk(query, params); } else { - try { - const response = await axios.post(`https://${HOST}:${PORT}/${language}`, `query=${encodeURIComponent(q)}`); - return response.data; + try { + let data = { + query: query, + parameters: params + }; + const response = await axios.post(`https://${HOST}:${PORT}/${HTTP_LANGUAGE}`, data); + return response.data; } catch (error) { console.error("Http query request failed: ", error.message); consoleOut("Trying with the AWS SDK"); - - if (NEPTUNE_TYPE == 'neptune-db') { - consoleOut("Trying with the AWS SDK"); - const response = await queryNeptuneSDK(q); - useSDK = true; - return response; - } - - throw new Error('AWS SDK for Neptune Analytics is not available, yet.'); + const response = await queryNeptuneSdk(query, params); + console.log('Querying via AWS SDK was successful, will use SDK for future queries') + useSDK = true; + return response; } - } + } } +/** + * Queries neptune using an SDK. + */ +async function queryNeptuneSdk(query, params = '{}') { + if (NEPTUNE_TYPE === NEPTUNE_DB) { + return await queryNeptuneDbSDK(query, params); + } else { + return await queryNeptuneGraphSDK(query, params); + } +} -async function queryNeptuneSDK(q) { +/** + * Queries neptune db using SDK (not to be used for neptune analytics). + */ +async function queryNeptuneDbSDK(query, params = '{}') { try { - const config = { - endpoint: `https://${HOST}:${PORT}` - }; - const client = new NeptunedataClient(config); + const client = getNeptunedataClient(); const input = { - openCypherQuery: q + openCypherQuery: query, + parameters: params }; const command = new ExecuteOpenCypherQueryCommand(input); const response = await client.send(command); return response; - } catch (error) { - console.error("SDK query request failed: ", error.message); + } catch (error) { + console.error("Neptune db SDK query request failed: ", error.message); + process.exit(1); + } +} + +/** + * Queries neptune analytics graph using SDK (not to be used for neptune db). + */ +async function queryNeptuneGraphSDK(query, params = '{}') { + try { + const client = getNeptuneGraphClient(); + const command = new ExecuteQueryCommand({ + graphIdentifier: NAME, + queryString: query, + language: NEPTUNE_GRAPH_LANGUAGE, + parameters: JSON.parse(params) + }); + const response = await client.send(command); + return await new Response(response.payload).json(); + } catch (error) { + console.error('Neptune graph SDK query request failed:' + JSON.stringify(error)); process.exit(1); } } @@ -106,7 +148,7 @@ async function queryNeptuneSDK(q) { async function getNodesNames() { let query = `MATCH (a) RETURN labels(a), count(a)`; - let response = await queryNeptune(query); + let response = await queryNeptune(query); try { response.results.forEach(result => { @@ -197,9 +239,9 @@ function addUpdateEdgeProperty(edgeName, name, value) { async function getEdgeProperties(edge) { - let query = `MATCH ()-[n:${edge.label}]->() RETURN properties(n) as properties LIMIT ${SAMPLE}`; + let query = `MATCH ()-[n:${edge.label}]->() RETURN properties(n) as properties LIMIT ${SAMPLE}`; try { - let response = await queryNeptune(query); + let response = await queryNeptune(query); let result = response.results; result.forEach(e => { Object.keys(e.properties).forEach(key => { @@ -221,9 +263,9 @@ async function getEdgesProperties() { async function getNodeProperties(node) { - let query = `MATCH (n:${node.label}) RETURN properties(n) as properties LIMIT ${SAMPLE}`; + let query = `MATCH (n:${node.label}) RETURN properties(n) as properties LIMIT ${SAMPLE}`; try { - let response = await queryNeptune(query); + let response = await queryNeptune(query); let result = response.results; result.forEach(e => { Object.keys(e.properties).forEach(key => { @@ -245,10 +287,10 @@ async function getNodesProperties() { async function checkEdgeDirectionCardinality(d) { - let queryFrom = `MATCH (from:${d.from})-[r:${d.edge.label}]->(to:${d.to}) WITH to, count(from) as rels WHERE rels > 1 RETURN rels LIMIT 1`; + let queryFrom = `MATCH (from:${d.from})-[r:${d.edge.label}]->(to:${d.to}) WITH to, count(from) as rels WHERE rels > 1 RETURN rels LIMIT 1`; let responseFrom = await queryNeptune(queryFrom); let resultFrom = responseFrom.results[0]; - let queryTo = `MATCH (from:${d.from})-[r:${d.edge.label}]->(to:${d.to}) WITH from, count(to) as rels WHERE rels > 1 RETURN rels LIMIT 1`; + let queryTo = `MATCH (from:${d.from})-[r:${d.edge.label}]->(to:${d.to}) WITH from, count(to) as rels WHERE rels > 1 RETURN rels LIMIT 1`; let responseTo = await queryNeptune(queryTo); let resultTo = responseTo.results[0]; let c = ''; @@ -290,18 +332,78 @@ function setGetNeptuneSchemaParameters(host, port, region, verbose = false, nept REGION = region; VERBOSE = verbose; NEPTUNE_TYPE = neptuneType; + NAME = parseNeptuneGraphName(host); } +function getNeptunedataClient() { + if (!neptunedataClient) { + console.log('Instantiating NeptunedataClient') + neptunedataClient = new NeptunedataClient({ + endpoint: `https://${HOST}:${PORT}` + }); + } + return neptunedataClient; +} + +function getNeptuneGraphClient() { + if (!neptuneGraphClient) { + console.log('Instantiating NeptuneGraphClient') + neptuneGraphClient = new NeptuneGraphClient({ + port: PORT, + host: parseNeptuneDomain(HOST), + region: REGION, + protocol: NEPTUNE_GRAPH_PROTOCOL, + }); + } + return neptuneGraphClient; +} -async function getSchemaViaSummaryAPI() { +/** + * Get a summary of a neptune analytics graph + */ +async function getNeptuneGraphSummary() { + console.log('Retrieving neptune graph summary') + const client = getNeptuneGraphClient(); + const command = new GetGraphSummaryCommand({ + graphIdentifier: NAME, + mode: 'detailed' + }); + const response = await client.send(command); + console.log('Retrieved neptune graph summary') + return response.graphSummary; +} + +/** + * Get a summary of a neptune db graph + */ +async function getNeptuneDbSummary() { + console.log('Retrieving neptune db summary') + let response = await axios.get(`https://${HOST}:${PORT}/propertygraph/statistics/summary`, { + params: { + mode: 'detailed' + } + }); + console.log('Retrieved neptune db summary') + return response.data.payload.graphSummary; +} + +/** + * Load the neptune schema by querying the summary API + */ +async function loadSchemaViaSummary() { try { - const response = await axios.get(`https://${HOST}:${PORT}/propertygraph/statistics/summary?mode=detailed`); - response.data.payload.graphSummary.nodeLabels.forEach(label => { + let graphSummary; + if (NEPTUNE_TYPE === NEPTUNE_DB) { + graphSummary = await getNeptuneDbSummary(); + } else { + graphSummary = await getNeptuneGraphSummary(); + } + graphSummary.nodeLabels.forEach(label => { schema.nodeStructures.push({label:label, properties:[]}); consoleOut(' Found node: ' + yellow(label)); }); - response.data.payload.graphSummary.edgeLabels.forEach(label => { + graphSummary.edgeLabels.forEach(label => { schema.edgeStructures.push({label:label, properties:[], directions:[]}); consoleOut(' Found edge: ' + yellow(label)); }); @@ -309,25 +411,26 @@ async function getSchemaViaSummaryAPI() { return true; } catch (error) { + console.error(`Getting the schema via Neptune Summary API failed: ${JSON.stringify(error)}`); return false; } } -async function getNeptuneSchema(quiet) { - +async function getNeptuneSchema(quiet) { + VERBOSE = !quiet; try { await getAWSCredentials(); - } catch (error) { + } catch (error) { consoleOut("There are no AWS credetials configured. \nGetting the schema from an Amazon Neptune database with IAM authentication works only with AWS credentials."); } - if (await getSchemaViaSummaryAPI()) { - consoleOut("Got nodes and edges via Neptune Summary API."); + if (await loadSchemaViaSummary()) { + consoleOut("Got nodes and edges via Neptune Summary API."); } else { - consoleOut("Getting nodes via queries."); + consoleOut("Getting nodes via queries."); await getNodesNames(); consoleOut("Getting edges via queries."); await getEdgesNames(); diff --git a/src/main.js b/src/main.js index 88f0d22..06cb5a5 100644 --- a/src/main.js +++ b/src/main.js @@ -37,6 +37,12 @@ const __dirname = path.dirname(__filename); // get version const version = JSON.parse(readFileSync(__dirname + '/../package.json')).version; +/** + * neptune-graph is neptune analytics + */ +const NEPTUNE_GRAPH = 'neptune-graph'; +const NEPTUNE_DB = 'neptune-db'; + // Input let quiet = false; let inputGraphQLSchema = ''; @@ -63,8 +69,7 @@ let inputCDKpipelineRegion = ''; let inputCDKpipelineDatabaseName = ''; let createLambdaZip = true; let outputFolderPath = './output'; -let neptuneType = 'neptune-db'; // or neptune-graph - +let neptuneType = NEPTUNE_DB; // or neptune-graph // Outputs let outputSchema = ''; @@ -248,6 +253,7 @@ function processArgs() { break; } }); + } async function main() { @@ -259,6 +265,8 @@ async function main() { processArgs(); + // Init output folder + mkdirSync(outputFolderPath, { recursive: true }); // Get graphDB schema from file if (inputGraphDBSchemaFile != '' && inputGraphQLSchema == '' && inputGraphQLSchemaFile == '') { try { @@ -271,10 +279,14 @@ async function main() { } // Check if Neptune target is db or graph - if ( inputGraphDBSchemaNeptuneEndpoint.includes('neptune-graph') || - createUpdatePipelineEndpoint.includes('neptune-graph') || - inputCDKpipelineEnpoint.includes('neptune-graph')) - neptuneType = 'neptune-graph'; + if (inputGraphDBSchemaNeptuneEndpoint.includes(NEPTUNE_GRAPH) || + createUpdatePipelineEndpoint.includes(NEPTUNE_GRAPH) || + inputCDKpipelineEnpoint.includes(NEPTUNE_GRAPH)) { + neptuneType = NEPTUNE_GRAPH; + // neptune analytics requires IAM + console.log("Detected neptune-graph from input endpoint - setting IAM auth to true as it is required for neptune analytics") + isNeptuneIAMAuth = true; + } // Get Neptune schema from endpoint if (inputGraphDBSchemaNeptuneEndpoint != '' && inputGraphDBSchema == '' && inputGraphDBSchemaFile == '') { @@ -288,11 +300,11 @@ async function main() { let neptuneRegionParts = inputGraphDBSchemaNeptuneEndpoint.split('.'); let neptuneRegion = ''; - if (neptuneType == 'neptune-db') + if (neptuneType === NEPTUNE_DB) neptuneRegion = neptuneRegionParts[2]; else neptuneRegion = neptuneRegionParts[1]; - + if (!quiet) console.log('Getting Neptune schema from endpoint: ' + yellow(neptuneHost + ':' + neptunePort)); setGetNeptuneSchemaParameters(neptuneHost, neptunePort, neptuneRegion, true); let startTime = performance.now(); @@ -351,14 +363,16 @@ async function main() { createUpdatePipelineRegion == '' && !createUpdatePipelineNeptuneDatabaseName == '') { console.error('AWS pipeline: a Neptune database region is required.'); process.exit(1); - } - if (createUpdatePipelineEndpoint != '' && createUpdatePipelineRegion == '') { + } + if (createUpdatePipelineEndpoint != '') { let parts = createUpdatePipelineEndpoint.split('.'); createUpdatePipelineNeptuneDatabaseName = parts[0]; - if (neptuneType === 'neptune-db') { - createUpdatePipelineRegion = parts[2]; - } else { - createUpdatePipelineRegion = parts[1]; + if (createUpdatePipelineRegion === '') { + if (neptuneType === NEPTUNE_DB) { + createUpdatePipelineRegion = parts[2]; + } else { + createUpdatePipelineRegion = parts[1]; + } } } if (createUpdatePipelineName == '') { @@ -419,8 +433,6 @@ async function main() { // Outputs // **************************************************************************** - mkdirSync(outputFolderPath, { recursive: true }); - // Output GraphQL schema no directives if (inputGraphQLSchema != '') { @@ -500,7 +512,6 @@ async function main() { try { writeFileSync(outputJSResolverFile, outputJSResolver); - //writeFileSync('./test/output.resolver.graphql.js', outputJSResolver); // Remove, for development and test only if (!quiet) console.log('Wrote Javascript resolver to file: ' + yellow(outputJSResolverFile)); } catch (err) { console.error('Error writing Javascript resolver to file: ' + outputJSResolverFile); @@ -513,7 +524,11 @@ async function main() { outputLambdaPackagePath = '/../templates/Lambda4AppSyncHTTP'; break; case 'sdk': - outputLambdaPackagePath = '/../templates/Lambda4AppSyncSDK'; + if (neptuneType === NEPTUNE_DB) { + outputLambdaPackagePath = '/../templates/Lambda4AppSyncSDK'; + } else { + outputLambdaPackagePath = '/../templates/Lambda4AppSyncGraphSDK'; + } break; } @@ -588,20 +603,22 @@ async function main() { inputCDKpipelineFile = `${outputFolderPath}/${inputCDKpipelineName}-cdk.js`; } - await createAWSpipelineCDK( inputCDKpipelineName, - inputCDKpipelineDatabaseName, - inputCDKpipelineRegion, - outputSchema, - schemaModel, - __dirname + outputLambdaPackagePath, - inputCDKpipelineFile, - __dirname, - quiet, - isNeptuneIAMAuth, - neptuneHost, - neptunePort, - outputFolderPath, - neptuneType); + await createAWSpipelineCDK({ + pipelineName: inputCDKpipelineName, + neptuneDBName: inputCDKpipelineDatabaseName, + neptuneDBregion: inputCDKpipelineRegion, + appSyncSchema: outputSchema, + schemaModel: schemaModel, + lambdaFilesPath: __dirname + outputLambdaPackagePath, + outputFile: inputCDKpipelineFile, + __dirname: __dirname, + quiet: quiet, + isNeptuneIAMAuth: isNeptuneIAMAuth, + neptuneHost: neptuneHost, + neptunePort: neptunePort, + outputFolderPath: outputFolderPath, + neptuneType: neptuneType + }); } catch (err) { console.error('Error creating CDK File: ' + err); } diff --git a/src/pipelineResources.js b/src/pipelineResources.js index cde627c..fee2b97 100644 --- a/src/pipelineResources.js +++ b/src/pipelineResources.js @@ -47,6 +47,9 @@ import fs from 'fs'; import archiver from 'archiver'; import ora from 'ora'; import { exit } from "process"; +import { parseNeptuneDomain } from "./util.js"; + +const NEPTUNE_DB = 'neptune-db'; // Input let NEPTUNE_DB_NAME = ''; @@ -72,7 +75,7 @@ let NEPTUNE_CURRENT_IAM = false; let NEPTUNE_IAM_POLICY_RESOURCE = '*'; let LAMBDA_ROLE = ''; let LAMBDA_ARN = ''; -let NEPTUNE_TYPE = 'neptune-db'; +let NEPTUNE_TYPE = NEPTUNE_DB; let ZIP = null; let RESOURCES = {}; let RESOURCES_FILE = ''; @@ -150,12 +153,14 @@ function storeResource(resource) { fs.writeFileSync(RESOURCES_FILE, JSON.stringify(RESOURCES, null, 2)); } - -async function getNeptuneClusterinfoBy(name, region) { +/** + * Retrieves information about the neptune db cluster for the given db name and region. Should not be used for neptune analytics graphs. + */ +async function getNeptuneClusterDbInfoBy(name, region) { NEPTUNE_DB_NAME = name; REGION = region; - await getNeptuneClusterinfo(); + await setNeptuneDbClusterInfo(); return { host: NEPTUNE_HOST, @@ -168,36 +173,34 @@ async function getNeptuneClusterinfoBy(name, region) { iamPolicyResource: NEPTUNE_IAM_POLICY_RESOURCE }; } +/** + * Retrieves information about the neptune db cluster and sets module-level variable values based on response data. Should not be used for neptune analytics graphs. + */ +async function setNeptuneDbClusterInfo() { + const neptuneClient = new NeptuneClient({region: REGION}); -async function getNeptuneClusterinfo() { - if (NEPTUNE_TYPE == 'neptune-db') { - const neptuneClient = new NeptuneClient({region: REGION}); + const params = { + DBClusterIdentifier: NEPTUNE_DB_NAME + }; - const params = { - DBClusterIdentifier: NEPTUNE_DB_NAME - }; + const data = await neptuneClient.send(new DescribeDBClustersCommand(params)); - const data = await neptuneClient.send(new DescribeDBClustersCommand(params)); - - const input = { // DescribeDBSubnetGroupsMessage - DBSubnetGroupName: data.DBClusters[0].DBSubnetGroup, - }; - const command = new DescribeDBSubnetGroupsCommand(input); - const response = await neptuneClient.send(command); - - NEPTUNE_HOST = data.DBClusters[0].Endpoint; - NEPTUNE_PORT = data.DBClusters[0].Port.toString(); - NEPTUNE_DBSubnetGroup = data.DBClusters[0].DBSubnetGroup; - NEPTUNE_VpcSecurityGroupId = data.DBClusters[0].VpcSecurityGroups[0].VpcSecurityGroupId; - NEPTUNE_CURRENT_IAM = data.DBClusters[0].IAMDatabaseAuthenticationEnabled; - NEPTUNE_CURRENT_VERSION = data.DBClusters[0].EngineVersion; - NEPTUNE_IAM_POLICY_RESOURCE = `${data.DBClusters[0].DBClusterArn.substring(0, data.DBClusters[0].DBClusterArn.lastIndexOf(':cluster')).replace('rds', 'neptune-db')}:${data.DBClusters[0].DbClusterResourceId}/*`; - response.DBSubnetGroups[0].Subnets.forEach(element => { - NEPTUNE_DBSubnetIds.push(element.SubnetIdentifier); - }); - } else { - throw new Error('AWS SDK for Neptune Analytics is not available, yet.'); - } + const input = { // DescribeDBSubnetGroupsMessage + DBSubnetGroupName: data.DBClusters[0].DBSubnetGroup, + }; + const command = new DescribeDBSubnetGroupsCommand(input); + const response = await neptuneClient.send(command); + + NEPTUNE_HOST = data.DBClusters[0].Endpoint; + NEPTUNE_PORT = data.DBClusters[0].Port.toString(); + NEPTUNE_DBSubnetGroup = data.DBClusters[0].DBSubnetGroup; + NEPTUNE_VpcSecurityGroupId = data.DBClusters[0].VpcSecurityGroups[0].VpcSecurityGroupId; + NEPTUNE_CURRENT_IAM = data.DBClusters[0].IAMDatabaseAuthenticationEnabled; + NEPTUNE_CURRENT_VERSION = data.DBClusters[0].EngineVersion; + NEPTUNE_IAM_POLICY_RESOURCE = `${data.DBClusters[0].DBClusterArn.substring(0, data.DBClusters[0].DBClusterArn.lastIndexOf(':cluster')).replace('rds', NEPTUNE_DB)}:${data.DBClusters[0].DbClusterResourceId}/*`; + response.DBSubnetGroups[0].Subnets.forEach(element => { + NEPTUNE_DBSubnetIds.push(element.SubnetIdentifier); + }); } @@ -217,7 +220,7 @@ async function createLambdaRole() { }, ], }), - RoleName: NAME +"LambdaExecutionRole" + RoleName: NAME +"LambdaExecutionRole" }; const data = await iamClient.send(new CreateRoleCommand(params)); //await waitUntilRoleExists({ client: iamClient, maxWaitTime: 180 }, { RoleName: data.Role.RoleName }); // does not work :(, using sleep @@ -241,7 +244,7 @@ async function createLambdaRole() { if (NEPTUME_IAM_AUTH) { let action = []; - if (NEPTUNE_TYPE == 'neptune-db') { + if (NEPTUNE_TYPE === NEPTUNE_DB) { action = [ "neptune-db:DeleteDataViaQuery", "neptune-db:connect", @@ -319,62 +322,40 @@ async function createDeploymentPackage(folderPath) { async function createLambdaFunction() { - const lambdaClient = new LambdaClient({region: REGION}); - if (!quiet) spinner = ora('Creating Lambda function ...').start(); - - let params; - if (NEPTUME_IAM_AUTH) { - params = { - Code: { - ZipFile: ZIP - }, - FunctionName: NAME +'LambdaFunction', - Handler: 'index.handler', - Role: LAMBDA_ROLE, - Runtime: 'nodejs18.x', - Description: 'Neptune GraphQL Resolver for AppSync', - Timeout: 15, - MemorySize: 128, - Environment: { - Variables: { - "NEPTUNE_HOST": NEPTUNE_HOST, - "NEPTUNE_PORT": NEPTUNE_PORT, - "NEPTUNE_IAM_AUTH_ENABLED": "true", - "LOGGING_ENABLED": "false", - "NEPTUNE_TYPE": NEPTUNE_TYPE - }, - }, - }; - } else { - params = { - Code: { - ZipFile: ZIP - }, + + let params = { + Code: { + ZipFile: ZIP + }, FunctionName: NAME +'LambdaFunction', - Handler: 'index.handler', - Role: LAMBDA_ROLE, - Runtime: 'nodejs18.x', - Description: 'Neptune GraphQL Resolver for AppSync', - Timeout: 15, - MemorySize: 128, - VpcConfig: { - SubnetIds: NEPTUNE_DBSubnetIds, - SecurityGroupIds: [NEPTUNE_VpcSecurityGroupId] - }, - Environment: { - Variables: { - "NEPTUNE_HOST": NEPTUNE_HOST, - "NEPTUNE_PORT": NEPTUNE_PORT, - "NEPTUNE_IAM_AUTH_ENABLED": "false", - "LOGGING_ENABLED": "false" - }, + Handler: 'index.handler', + Role: LAMBDA_ROLE, + Runtime: 'nodejs18.x', + Description: 'Neptune GraphQL Resolver for AppSync', + Timeout: 15, + MemorySize: 128, + Environment: { + Variables: { + "NEPTUNE_HOST": NEPTUNE_HOST, + "NEPTUNE_PORT": NEPTUNE_PORT, + "NEPTUNE_IAM_AUTH_ENABLED": NEPTUME_IAM_AUTH.toString(), + "LOGGING_ENABLED": "false", + "NEPTUNE_DB_NAME": NEPTUNE_DB_NAME, + "NEPTUNE_REGION": REGION, + "NEPTUNE_DOMAIN": parseNeptuneDomain(NEPTUNE_HOST), }, - }; - } + }, + }; - const data = await lambdaClient.send(new LambdaCreateFunctionCommand(params)); - //await sleep(5000); + if (!NEPTUME_IAM_AUTH) { + params.VpcConfig = { + SubnetIds: NEPTUNE_DBSubnetIds, + SecurityGroupIds: [NEPTUNE_VpcSecurityGroupId] + } + } + const lambdaClient = new LambdaClient({region: REGION}); + const data = await lambdaClient.send(new LambdaCreateFunctionCommand(params)); LAMBDA_ARN = data.FunctionArn; storeResource({LambdaFunction: NAME +'LambdaFunction'}); if (!quiet) spinner.succeed('Lambda Name: ' + yellow(NAME +'LambdaFunction') + ' ARN: ' + yellow(LAMBDA_ARN)); @@ -407,7 +388,7 @@ async function createAppSyncAPI() { storeResource({LambdaInvokePolicy: policyARN}); if (!quiet) spinner.succeed('Lambda invocation policy ARN: ' + yellow(policyARN)); - let params = { + let params = { AssumeRolePolicyDocument: JSON.stringify({ Version: "2012-10-17", Statement: [ @@ -420,7 +401,7 @@ async function createAppSyncAPI() { } ] }), - RoleName: NAME +"LambdaInvocationRole" + RoleName: NAME +"LambdaInvocationRole" }; if (!quiet) spinner = ora('Creating role for Lambda invocation ...').start(); @@ -806,9 +787,22 @@ async function updateAppSyncAPI(resources) { } -async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBregion, appSyncSchema, schemaModel, lambdaFilesPath, addMutations, quietI, __dirname, isNeptuneIAMAuth, neptuneHost, neptunePort, outputFolderPath, neptuneType) { - - NAME = pipelineName; +async function createUpdateAWSpipeline ( pipelineName, + neptuneDBName, + neptuneDBregion, + appSyncSchema, + schemaModel, + lambdaFilesPath, + addMutations, + quietI, + __dirname, + isNeptuneIAMAuth, + neptuneHost, + neptunePort, + outputFolderPath, + neptuneType) { + + NAME = pipelineName; REGION = neptuneDBregion; NEPTUNE_DB_NAME = neptuneDBName; APPSYNC_SCHEMA = appSyncSchema; @@ -830,11 +824,11 @@ async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBre try { storeResource({region: REGION}); - if (NEPTUNE_TYPE === 'neptune-db') { + if (NEPTUNE_TYPE === NEPTUNE_DB) { try { if (!quiet) console.log('Get Neptune Cluster Info'); if (!quiet) spinner = ora('Getting ...').start(); - await getNeptuneClusterinfo(); + await setNeptuneDbClusterInfo(); if (!quiet) spinner.succeed('Got Neptune Cluster Info'); if (isNeptuneIAMAuth) { if (!NEPTUNE_CURRENT_IAM) { @@ -886,14 +880,14 @@ async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBre if (!quiet) console.log('Create AppSync API'); await createAppSyncAPI(); - + if (!quiet) console.log('Saved resorces to file: ' + yellow(RESOURCES_FILE)); } catch (error) { if (!quiet) spinner.fail('Error creating resources: ' + error); console.error('Rolling back resources.'); - await removeAWSpipelineResources(RESOURCES, quiet); - return; + await removeAWSpipelineResources(RESOURCES, quiet); + return; } } else { @@ -921,5 +915,5 @@ async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBre } } -export { createUpdateAWSpipeline, getNeptuneClusterinfoBy, removeAWSpipelineResources } +export { createUpdateAWSpipeline, getNeptuneClusterDbInfoBy, removeAWSpipelineResources } diff --git a/src/test/util.test.js b/src/test/util.test.js new file mode 100644 index 0000000..906ea71 --- /dev/null +++ b/src/test/util.test.js @@ -0,0 +1,25 @@ +import {parseNeptuneDomain, parseNeptuneGraphName} from '../util.js'; + +test('parse domain from neptune cluster host', () => { + expect(parseNeptuneDomain('db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com')).toBe('neptune.amazonaws.com'); +}); + +test('parse domain from neptune analytics host', () => { + expect(parseNeptuneDomain('g-abcdef.us-west-2.neptune-graph.amazonaws.com')).toBe('neptune-graph.amazonaws.com'); +}); + +test('parse domain from host without enough parts throws error', () => { + expect(() => parseNeptuneDomain('invalid.com')).toThrow('Cannot parse neptune host invalid.com because it has 2 parts but expected at least 5'); +}); + +test('parse name from neptune cluster host', () => { + expect(parseNeptuneGraphName('db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com')).toBe('db-neptune-abc-def'); +}); + +test('parse name from neptune analytics host', () => { + expect(parseNeptuneGraphName('g-abcdef.us-west-2.neptune-graph.amazonaws.com')).toBe('g-abcdef'); +}); + +test('parse name from host without enough parts throws error', () => { + expect(() => parseNeptuneGraphName('invalid.com')).toThrow('Cannot parse neptune host invalid.com because it has 2 parts but expected at least 5'); +}); \ No newline at end of file diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..31ea3b3 --- /dev/null +++ b/src/util.js @@ -0,0 +1,60 @@ +/* +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 MIN_HOST_PARTS = 5; +const NUM_DOMAIN_PARTS = 3; +const DELIMITER = '.'; + +/** + * Splits a neptune host into its parts, throwing an Error if there are unexpected number of parts. + * + * @param neptuneHost + */ +function splitHost(neptuneHost) { + let parts = neptuneHost.split(DELIMITER); + if (parts.length < MIN_HOST_PARTS) { + throw Error('Cannot parse neptune host ' + neptuneHost + ' because it has ' + parts.length + ' parts but expected at least ' + MIN_HOST_PARTS); + } + return parts; +} + +/** + * Parses the domain from the given neptune db or neptune analytics host. + * + * Example: g-abcdef.us-west-2.neptune-graph.amazonaws.com ==> neptune-graph.amazonaws.com + * Example: db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com ==> neptune.amazonaws.com + * + * @param neptuneHost + */ +function parseNeptuneDomain(neptuneHost) { + let parts = splitHost(neptuneHost); + // last 3 parts of the host make up the domain + // ie. neptune.amazonaws.com or neptune-graph.amazonaws.com + let domainParts = parts.splice(parts.length - NUM_DOMAIN_PARTS, NUM_DOMAIN_PARTS); + return domainParts.join(DELIMITER); +} + +/** + * Parses the neptune graph name from the given neptune db or neptune analytics host. + * + * Example: g-abcdef.us-west-2.neptune-graph.amazonaws.com ==> g-abcdef + * Example: db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com ==> db-neptune-abc-def + * + * @param neptuneHost + */ +function parseNeptuneGraphName(neptuneHost) { + let parts = splitHost(neptuneHost); + // graph name is the first part + return parts[0]; +} + +export {parseNeptuneDomain, parseNeptuneGraphName}; \ No newline at end of file diff --git a/templates/CDKTemplate.js b/templates/CDKTemplate.js index e17c24a..ce9d0a9 100644 --- a/templates/CDKTemplate.js +++ b/templates/CDKTemplate.js @@ -14,6 +14,7 @@ const { Stack, Duration, App } = require('aws-cdk-lib'); const lambda = require( 'aws-cdk-lib/aws-lambda'); const iam = require( 'aws-cdk-lib/aws-iam'); const ec2 = require( 'aws-cdk-lib/aws-ec2'); +const { parseNeptuneDomain } = require('../src/util.js'); const { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema, CfnDataSource, CfnResolver, CfnFunctionConfiguration } = require( 'aws-cdk-lib/aws-appsync'); const NAME = ''; @@ -21,6 +22,8 @@ const REGION = ''; const NEPTUNE_HOST = ''; const NEPTUNE_PORT = ''; +const NEPTUNE_DB_NAME = ''; +const NEPTUNE_TYPE = ''; const NEPTUNE_DBSubnetGroup = null; const NEPTUNE_IAM_AUTH = false; const NEPTUNE_IAM_POLICY_RESOURCE = '*'; @@ -54,7 +57,17 @@ class AppSyncNeptuneStack extends Stack { lambda_role.addManagedPolicy( iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')); - if (NEPTUNE_IAM_AUTH) { + let env = { + NEPTUNE_HOST: NEPTUNE_HOST, + NEPTUNE_PORT: NEPTUNE_PORT, + NEPTUNE_IAM_AUTH_ENABLED: NEPTUNE_IAM_AUTH.toString(), + LOGGING_ENABLED: 'false', + NEPTUNE_DB_NAME: NEPTUNE_DB_NAME, + NEPTUNE_REGION: REGION, + NEPTUNE_DOMAIN: parseNeptuneDomain(NEPTUNE_HOST), + NEPTUNE_TYPE: NEPTUNE_TYPE, + }; + if (NEPTUNE_IAM_AUTH) { // is IAM auth echoLambda = new lambda.Function(this, LAMBDA_FUNCTION_NAME, { functionName: LAMBDA_FUNCTION_NAME, @@ -64,12 +77,7 @@ class AppSyncNeptuneStack extends Stack { runtime: lambda.Runtime.NODEJS_18_X, timeout: Duration.seconds(15), memorySize: 128, - environment: { - NEPTUNE_HOST: NEPTUNE_HOST, - NEPTUNE_PORT: NEPTUNE_PORT, - NEPTUNE_IAM_AUTH_ENABLED: 'true', - LOGGING_ENABLED: 'false' - }, + environment: env, initialPolicy: [new iam.PolicyStatement({ sid: NAME + "NeptuneQueryPolicy", effect: iam.Effect.ALLOW, @@ -100,12 +108,7 @@ class AppSyncNeptuneStack extends Stack { runtime: lambda.Runtime.NODEJS_18_X, timeout: Duration.seconds(15), memorySize: 128, - environment: { - NEPTUNE_HOST: NEPTUNE_HOST, - NEPTUNE_PORT: NEPTUNE_PORT, - NEPTUNE_IAM_AUTH_ENABLED: 'false', - LOGGING_ENABLED: 'false' - }, + environment: env, vpc: neptune_vpc, allowPublicSubnet: 'true', roleArn: lambda_role.roleArn diff --git a/templates/Lambda4AppSyncGraphSDK/index.mjs b/templates/Lambda4AppSyncGraphSDK/index.mjs new file mode 100644 index 0000000..dd3bf73 --- /dev/null +++ b/templates/Lambda4AppSyncGraphSDK/index.mjs @@ -0,0 +1,93 @@ +import {ExecuteQueryCommand, NeptuneGraphClient} from "@aws-sdk/client-neptune-graph"; +import {resolveGraphDBQueryFromAppSyncEvent} from './output.resolver.graphql.js'; + +const PROTOCOL = 'https'; +const QUERY_LANGUAGE = 'OPEN_CYPHER'; +const RESOLVER_LANGUAGE = 'opencypher'; + +let client; + +function getClient() { + if (!client) { + try { + log('Instantiating NeptuneGraphClient') + client = new NeptuneGraphClient({ + port: process.env.NEPTUNE_PORT, + host: process.env.NEPTUNE_DOMAIN, + region: process.env.NEPTUNE_REGION, + protocol: PROTOCOL, + }); + } catch (error) { + return onError('Error instantiating NeptuneGraphClient: ', error); + } + } + return client; +} + +function onError(context, error) { + let msg; + if (error) { + msg = context + ':' + error.message; + } else { + msg = context; + } + console.error(msg); + if (error) { + throw error; + } + throw new Error(msg); +} + +function log(message) { + if (process.env.LOGGING_ENABLED) { + console.log(message); + } +} + +/** + * Converts graphQL query to open cypher. + */ +function resolveGraphQuery(event) { + try { + let resolver = resolveGraphDBQueryFromAppSyncEvent(event); + if (resolver.language !== RESOLVER_LANGUAGE) { + return onError('Unsupported resolver language:' + resolver.language) + } + log('Resolved ' + resolver.language + ' query successfully'); + return resolver; + } catch (error) { + return onError('Error resolving graphQL query', error); + } +} + +/** + * Converts incoming graphQL query into open cypher format and sends the query to neptune analytics query API. + */ +export const handler = async (event) => { + let resolver = resolveGraphQuery(event); + + try { + const command = new ExecuteQueryCommand({ + graphIdentifier: process.env.NEPTUNE_DB_NAME, + queryString: resolver.query, + language: QUERY_LANGUAGE, + parameters: resolver.parameters + }); + const response = await getClient().send(command); + log('Received query response'); + let data = await new Response(response.payload).json(); + // query result should have result array of single item or an empty array + // {"results": [{ ... }]} + if (data.results.length === 0) { + log('Query produced no results'); + return []; + } + if (data.results.length !== 1) { + return onError('Expected 1 query result but received ' + data.results.length); + } + log('Obtained data from query response'); + return data.results[0][Object.keys(data.results[0])[0]]; + } catch (error) { + return onError('Error executing ' + QUERY_LANGUAGE + ' query: ', error); + } +}; \ No newline at end of file diff --git a/templates/Lambda4AppSyncGraphSDK/package.json b/templates/Lambda4AppSyncGraphSDK/package.json new file mode 100644 index 0000000..610f5e8 --- /dev/null +++ b/templates/Lambda4AppSyncGraphSDK/package.json @@ -0,0 +1,15 @@ +{ + "name": "lambda4appsyncgraphsdk", + "version": "1.0.0", + "description": "AWS Lambda function to bridge AppSync to Neptune Analytics", + "main": "index.mjs", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "AWS", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-neptune-graph": "3.662.0", + "graphql-tag": "2.12.6" + } +} diff --git a/test/TestCases/.DS_Store b/test/TestCases/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..b5d81508a564ac05708ef3b15239364c882d559c GIT binary patch literal 6148 zcmeHKyN&`e4733uB$_R+%qKua8*%Uj`vZq`CmIA22Wg9eQyXA;Mg zC{wK0BBIOl_gthAkqO*Tt~T_|_RV`X$cO^rIAbP@OMBe!4*P8{`*FayWBHVgoaE;Z z-}Y!!fC^9nDnJFOz;hMI`Z}3D_gJ1r1*pIqC}7`*0ynIQU7&wDFn9|993kw6x%U#l zVgX=H>;e&iX;6Vd)od{|=!lohtBGA;&_%QP(7ai*Ls7pS=NC^Gt$`e=02R1bU>M8B z>i-)4Pyc^U;))7TfwxjXN2_+V#FMhN_8w=ow!pvOmh%fY!`vwtyc`3)9AjbS_`{PT auh<;>HL(kHI^s?T@@K$wp;3WXD{uqNbrg{R literal 0 HcmV?d00001 diff --git a/test/TestCases/Case07/.DS_Store b/test/TestCases/Case07/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1b83a3bed89e754471532e37c85dffb2fb73f08e GIT binary patch literal 6148 zcmeHKF>V4u473vzA<-Fqv zr#PR@%ooS)huPfBrf{O2IgE|_^pU+(#DQ>}akQV)`g{L**zZQ!e+S5Yvjds-b;IYL zObSQ=DIf);fE0MF0##q9v&SB*)1-hDcmf6ZeQ0oEFB}r%(}5vc0N@1SFwCQu05%4I zy>LiG1m;NvCe^FO@T4Q&Dz6s~iAguF=ELh|uMWlIcAVcL-MlAilmb%VT7lPG&RPFg z@H_qgHAyQeAO)UE0iP__%LSfPwRQA3*4hT&z?t(4r(qrx3{j4OQI4@-Iew3%%xj!u WzZVXPK}S63K>Z9*7nv0JZv`%`G!@|h literal 0 HcmV?d00001 diff --git a/test/TestCases/Case07/Case07.01.test.js b/test/TestCases/Case07/Case07.01.test.js new file mode 100644 index 0000000..fd7017c --- /dev/null +++ b/test/TestCases/Case07/Case07.01.test.js @@ -0,0 +1,13 @@ +import {readJSONFile} from '../../testLib'; +import {main} from "../../../src/main"; + +const casetest = readJSONFile('./test/TestCases/Case07/case01.json'); + +async function executeUtility() { + process.argv = casetest.argv; + await main(); +} + +test('Execute utility: ' + casetest.argv.join(' '), async () => { + expect(await executeUtility()).not.toBe(null); +}, 600000); diff --git a/test/TestCases/Case07/Case07.02.test.js b/test/TestCases/Case07/Case07.02.test.js new file mode 100644 index 0000000..963e01a --- /dev/null +++ b/test/TestCases/Case07/Case07.02.test.js @@ -0,0 +1,6 @@ +import {checkOutputFilesContent, checkOutputFilesSize, checkOutputZipLambdaUsesSdk, readJSONFile} from '../../testLib'; + +const casetest = readJSONFile('./test/TestCases/Case07/case01.json'); +checkOutputFilesSize('./test/TestCases/Case07/output', casetest.testOutputFilesSize, './test/TestCases/Case07/outputReference'); +checkOutputFilesContent('./test/TestCases/Case07/output', casetest.testOutputFilesContent, './test/TestCases/Case07/outputReference'); +checkOutputZipLambdaUsesSdk('./test/TestCases/Case07/output', './test/TestCases/Case07/output/AirportsJestSDKTest.zip'); diff --git a/test/TestCases/Case07/Case07.03.test.js b/test/TestCases/Case07/Case07.03.test.js new file mode 100644 index 0000000..129ed33 --- /dev/null +++ b/test/TestCases/Case07/Case07.03.test.js @@ -0,0 +1,13 @@ +import { readJSONFile } from '../../testLib'; +import { main } from "../../../src/main"; + +const casetest = readJSONFile('./test/TestCases/Case07/case02.json'); + +async function executeUtility() { + process.argv = casetest.argv; + await main(); +} + +test('Execute utility: ' + casetest.argv.join(' '), async () => { + expect(await executeUtility()).not.toBe(null); +}, 600000); diff --git a/test/TestCases/Case07/case01.json b/test/TestCases/Case07/case01.json new file mode 100644 index 0000000..5cbe13a --- /dev/null +++ b/test/TestCases/Case07/case01.json @@ -0,0 +1,18 @@ +{ + "name": "Unit Test (Air Routes) Pipeline", + "description":"Create SDK pipeline", + "argv":["--quiet", + "--input-schema-file", "./test/TestCases/airports.source.schema.graphql", + "--output-folder-path", "./test/TestCases/Case07/output", + "--output-schema-file", "./test/TestCases/Case07/output/output.schema.graphql", + "--output-source-schema-file", "./test/TestCases/Case07/output/output.source.schema.graphql", + "--output-js-resolver-file", "./test/TestCases/Case07/output/output.resolver.graphql.js", + "--create-update-aws-pipeline", + "--create-update-aws-pipeline-name", "AirportsJestSDKTest", + "--create-update-aws-pipeline-neptune-endpoint", ":", + "--output-resolver-query-sdk"], + "host": "", + "port": "", + "testOutputFilesSize": ["output.resolver.graphql.js"], + "testOutputFilesContent": ["output.schema.graphql", "output.source.schema.graphql"] +} \ No newline at end of file diff --git a/test/TestCases/Case07/case02.json b/test/TestCases/Case07/case02.json new file mode 100644 index 0000000..77f4337 --- /dev/null +++ b/test/TestCases/Case07/case02.json @@ -0,0 +1,9 @@ +{ + "name": "Unit Test (Air Routes) Remove Pipeline", + "description":"Remove SDK pipeline", + "argv":["--quiet", + "--remove-aws-pipeline-name", "AirportsJestSDKTest", + "--output-folder-path", "./test/TestCases/Case07/output"], + "host": "", + "port": "" +} \ No newline at end of file diff --git a/test/TestCases/Case07/outputReference/output.resolver.graphql.js b/test/TestCases/Case07/outputReference/output.resolver.graphql.js new file mode 100644 index 0000000..1a6a30d --- /dev/null +++ b/test/TestCases/Case07/outputReference/output.resolver.graphql.js @@ -0,0 +1,4536 @@ +/* +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; + +// 2023-10-10T23:49:35.620Z + +const schemaDataModelJSON = `{ + "kind": "Document", + "definitions": [ + { + "kind": "ObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "Continent" + }, + "interfaces": [], + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "alias" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "property" + }, + "value": { + "kind": "StringValue", + "value": "continent", + "block": false + } + } + ] + } + ], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "code" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "type" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "desc" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "airportContainssOut" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "AirportInput" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "options" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Options" + } + }, + "directives": [] + } + ], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "relationship" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "edgeType" + }, + "value": { + "kind": "StringValue", + "value": "contains", + "block": false + } + }, + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "direction" + }, + "value": { + "kind": "EnumValue", + "value": "OUT" + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "contains" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Contains" + } + }, + "directives": [] + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "ContinentInput" + }, + "directives": [], + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [] + } + ] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "code" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "type" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "desc" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "Country" + }, + "interfaces": [], + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "alias" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "property" + }, + "value": { + "kind": "StringValue", + "value": "country", + "block": false + } + } + ] + } + ], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "code" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "type" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "desc" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "airportContainssOut" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "AirportInput" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "options" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Options" + } + }, + "directives": [] + } + ], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "relationship" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "edgeType" + }, + "value": { + "kind": "StringValue", + "value": "contains", + "block": false + } + }, + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "direction" + }, + "value": { + "kind": "EnumValue", + "value": "OUT" + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "contains" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Contains" + } + }, + "directives": [] + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "CountryInput" + }, + "directives": [], + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [] + } + ] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "code" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "type" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "desc" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "Version" + }, + "interfaces": [], + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "alias" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "property" + }, + "value": { + "kind": "StringValue", + "value": "version", + "block": false + } + } + ] + } + ], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "date" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "code" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "author" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "type" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "desc" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "VersionInput" + }, + "directives": [], + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [] + } + ] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "date" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "code" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "author" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "type" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "desc" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "Airport" + }, + "interfaces": [], + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "alias" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "property" + }, + "value": { + "kind": "StringValue", + "value": "airport", + "block": false + } + } + ] + } + ], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "country" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "longest" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "code" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "city" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "elev" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "icao" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "lon" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "runways" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "region" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "type" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "lat" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "desc2" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "alias" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "property" + }, + "value": { + "kind": "StringValue", + "value": "desc", + "block": false + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "outboundRoutesCount" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "graphQuery" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "statement" + }, + "value": { + "kind": "StringValue", + "value": "MATCH (this)-[r:route]->(a) RETURN count(r)", + "block": false + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "continentContainsIn" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Continent" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "relationship" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "edgeType" + }, + "value": { + "kind": "StringValue", + "value": "contains", + "block": false + } + }, + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "direction" + }, + "value": { + "kind": "EnumValue", + "value": "IN" + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "countryContainsIn" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Country" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "relationship" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "edgeType" + }, + "value": { + "kind": "StringValue", + "value": "contains", + "block": false + } + }, + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "direction" + }, + "value": { + "kind": "EnumValue", + "value": "IN" + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "airportRoutesOut" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "AirportInput" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "options" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Options" + } + }, + "directives": [] + } + ], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "relationship" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "edgeType" + }, + "value": { + "kind": "StringValue", + "value": "route", + "block": false + } + }, + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "direction" + }, + "value": { + "kind": "EnumValue", + "value": "OUT" + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "airportRoutesIn" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "AirportInput" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "options" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Options" + } + }, + "directives": [] + } + ], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "relationship" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "edgeType" + }, + "value": { + "kind": "StringValue", + "value": "route", + "block": false + } + }, + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "direction" + }, + "value": { + "kind": "EnumValue", + "value": "IN" + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "contains" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Contains" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "route" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Route" + } + }, + "directives": [] + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "AirportInput" + }, + "directives": [], + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [] + } + ] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "country" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "longest" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "code" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "city" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "elev" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "icao" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "lon" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "runways" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "region" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "type" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "lat" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Float" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "desc" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "Contains" + }, + "interfaces": [], + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "alias" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "property" + }, + "value": { + "kind": "StringValue", + "value": "contains", + "block": false + } + } + ] + } + ], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [] + } + ] + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "Route" + }, + "interfaces": [], + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "alias" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "property" + }, + "value": { + "kind": "StringValue", + "value": "route", + "block": false + } + } + ] + } + ], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "arguments": [], + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "id" + }, + "arguments": [] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "dist" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + }, + "directives": [] + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "RouteInput" + }, + "directives": [], + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "dist" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + }, + "directives": [] + } + ] + }, + { + "kind": "InputObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "Options" + }, + "directives": [], + "fields": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "limit" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + }, + "directives": [] + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "Query" + }, + "interfaces": [], + "directives": [], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "code" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getAirportConnection" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "fromCode" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "toCode" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "cypher" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "statement" + }, + "value": { + "kind": "StringValue", + "value": "MATCH (:airport{code: '$fromCode'})-[:route]->(this:airport)-[:route]->(:airport{code:'$toCode'})", + "block": false + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getAirportWithGremlin" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "code" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "graphQuery" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "statement" + }, + "value": { + "kind": "StringValue", + "value": "g.V().has('airport', 'code', '$code').elementMap()", + "block": false + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getContinentsWithGremlin" + }, + "arguments": [], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Continent" + } + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "graphQuery" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "statement" + }, + "value": { + "kind": "StringValue", + "value": "g.V().hasLabel('continent').elementMap().fold()", + "block": false + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getCountriesCountGremlin" + }, + "arguments": [], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "graphQuery" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "statement" + }, + "value": { + "kind": "StringValue", + "value": "g.V().hasLabel('country').count()", + "block": false + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getNodeContinent" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ContinentInput" + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Continent" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getNodeContinents" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ContinentInput" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "options" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Options" + } + }, + "directives": [] + } + ], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Continent" + } + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getNodeCountry" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "CountryInput" + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Country" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getNodeCountrys" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "CountryInput" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "options" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Options" + } + }, + "directives": [] + } + ], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Country" + } + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getNodeVersion" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "VersionInput" + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Version" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getNodeVersions" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "VersionInput" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "options" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Options" + } + }, + "directives": [] + } + ], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Version" + } + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getNodeAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "AirportInput" + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "getNodeAirports" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "filter" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "AirportInput" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "options" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Options" + } + }, + "directives": [] + } + ], + "type": { + "kind": "ListType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + } + }, + "directives": [] + } + ] + }, + { + "kind": "ObjectTypeDefinition", + "name": { + "kind": "Name", + "value": "Mutation" + }, + "interfaces": [], + "directives": [], + "fields": [ + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "createAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "input" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "AirportInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "graphQuery" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "statement" + }, + "value": { + "kind": "StringValue", + "value": "CREATE (this:airport {$input}) RETURN this", + "block": false + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "addRoute" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "fromAirportCode" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "toAirportCode" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "String" + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "dist" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Route" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "graphQuery" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "statement" + }, + "value": { + "kind": "StringValue", + "value": "MATCH (from:airport{code:'$fromAirportCode'}), (to:airport{code:'$toAirportCode'}) CREATE (from)-[this:route{dist:$dist}]->(to) RETURN this", + "block": false + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "deleteAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "id" + }, + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Int" + } + }, + "directives": [ + { + "kind": "Directive", + "name": { + "kind": "Name", + "value": "graphQuery" + }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "statement" + }, + "value": { + "kind": "StringValue", + "value": "MATCH (this:airport) WHERE ID(this) = '$id' DETACH DELETE this", + "block": false + } + } + ] + } + ] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "createNodeContinent" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "input" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ContinentInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Continent" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "updateNodeContinent" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "input" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ContinentInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Continent" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "deleteNodeContinent" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "createNodeCountry" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "input" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "CountryInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Country" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "updateNodeCountry" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "input" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "CountryInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Country" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "deleteNodeCountry" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "createNodeVersion" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "input" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "VersionInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Version" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "updateNodeVersion" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "input" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "VersionInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Version" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "deleteNodeVersion" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "createNodeAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "input" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "AirportInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "updateNodeAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "input" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "AirportInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Airport" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "deleteNodeAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "connectNodeContinentToNodeAirportEdgeContains" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "from_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "to_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Contains" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "deleteEdgeContainsFromContinentToAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "from_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "to_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "connectNodeCountryToNodeAirportEdgeContains" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "from_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "to_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Contains" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "deleteEdgeContainsFromCountryToAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "from_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "to_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "connectNodeAirportToNodeAirportEdgeRoute" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "from_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "to_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "edge" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "RouteInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Route" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "updateEdgeRouteFromAirportToAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "from_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "to_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "edge" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "RouteInput" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Route" + } + }, + "directives": [] + }, + { + "kind": "FieldDefinition", + "name": { + "kind": "Name", + "value": "deleteEdgeRouteFromAirportToAirport" + }, + "arguments": [ + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "from_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + }, + { + "kind": "InputValueDefinition", + "name": { + "kind": "Name", + "value": "to_id" + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "ID" + } + } + }, + "directives": [] + } + ], + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Boolean" + } + }, + "directives": [] + } + ] + }, + { + "kind": "SchemaDefinition", + "directives": [], + "operationTypes": [ + { + "kind": "OperationTypeDefinition", + "operation": "query", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Query" + } + } + }, + { + "kind": "OperationTypeDefinition", + "operation": "mutation", + "type": { + "kind": "NamedType", + "name": { + "kind": "Name", + "value": "Mutation" + } + } + } + ] + } + ], + "loc": { + "start": 0, + "end": 4915 + } +}`; + +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}'; + + let 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); + + let lastNamePathContent = ''; + 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) { + let inputFields = transformFunctionInputParameters(obj.definitions[0].selectionSet.selections[0].arguments[0].value.fields, querySchemaInfo); + let 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); + } + let ocQuery = `CREATE (${nodeName}:${querySchemaInfo.returnTypeAlias} {${inputFields.fields}})\nRETURN ${returnBlock}`; + return ocQuery; + } + + // updateNode + if (querySchemaInfo.name.startsWith('updateNode') && querySchemaInfo.graphQuery == null) { + let inputFields = transformFunctionInputParameters(obj.definitions[0].selectionSet.selections[0].arguments[0].value.fields, querySchemaInfo); + let nodeID = inputFields.graphIdValue; + let 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: + let 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}); + let ocQuery = `MATCH (${nodeName})\nWHERE ID(${nodeName}) = $${param}\nSET ${setString}\nRETURN ${returnBlock}`; + return ocQuery; + } + + // deleteNode + if (querySchemaInfo.name.startsWith('deleteNode') && querySchemaInfo.graphQuery == null) { + let nodeID = obj.definitions[0].selectionSet.selections[0].arguments[0].value.value; + let nodeName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; + let param = nodeName + '_' + 'whereId'; + Object.assign(parameters, {[param]: nodeID}); + let 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; + let edgeType = querySchemaInfo.name.match(new RegExp('Edge' + "(.*)" + ''))[1]; + let edgeName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; + let egdgeTypeAlias = getTypeAlias(edgeType); + let 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) { + let inputFields = transformFunctionInputParameters(obj.definitions[0].selectionSet.selections[0].arguments[2].value.fields, querySchemaInfo); + let ocQuery = `MATCH (from), (to)\nWHERE ID(from) = $${paramFromId} AND ID(to) = $${paramToId}\nCREATE (from)-[${edgeName}:${egdgeTypeAlias}{${inputFields.fields}}]->(to)\nRETURN ${returnBlock}`; + return ocQuery; + } else { + let 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); + let inputFields = transformFunctionInputParameters(obj.definitions[0].selectionSet.selections[0].arguments[2].value.fields, querySchemaInfo); + let 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); + } + let 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); + + let paramFromId = edgeName + '_' + 'whereFromId'; + let paramToId = edgeName + '_' + 'whereToId'; + Object.assign(parameters, {[paramFromId]: fromID}); + Object.assign(parameters, {[paramToId]: toID}); + + let 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; + let edgeName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; + + let paramFromId = edgeName + '_' + 'whereFromId'; + let paramToId = edgeName + '_' + 'whereToId'; + Object.assign(parameters, {[paramFromId]: fromID}); + Object.assign(parameters, {[paramToId]: toID}); + + let 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')) { + let 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 '); + let entityName = querySchemaInfo.name + '_' + querySchemaInfo.returnType; + let body = statements[0].replace("this", entityName); + let 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 }; diff --git a/test/TestCases/Case07/outputReference/output.schema.graphql b/test/TestCases/Case07/outputReference/output.schema.graphql new file mode 100644 index 0000000..869ccc6 --- /dev/null +++ b/test/TestCases/Case07/outputReference/output.schema.graphql @@ -0,0 +1,151 @@ +type Continent { + id: ID! + code: String + type: String + desc: String + airportContainssOut(filter: AirportInput, options: Options): [Airport] + contains: Contains +} + +input ContinentInput { + id: ID + code: String + type: String + desc: String +} + +type Country { + _id: ID! + code: String + type: String + desc: String + airportContainssOut(filter: AirportInput, options: Options): [Airport] + contains: Contains +} + +input CountryInput { + _id: ID + code: String + type: String + desc: String +} + +type Version { + _id: ID! + date: String + code: String + author: String + type: String + desc: String +} + +input VersionInput { + _id: ID + date: String + code: String + author: String + type: String + desc: String +} + +type Airport { + _id: ID! + country: String + longest: Float + code: String + city: String + elev: Float + icao: String + lon: Float + runways: Float + region: String + type: String + lat: Float + desc2: String + outboundRoutesCount: Int + continentContainsIn: Continent + countryContainsIn: Country + airportRoutesOut(filter: AirportInput, options: Options): [Airport] + airportRoutesIn(filter: AirportInput, options: Options): [Airport] + contains: Contains + route: Route +} + +input AirportInput { + _id: ID + country: String + longest: Float + code: String + city: String + elev: Float + icao: String + lon: Float + runways: Float + region: String + type: String + lat: Float + desc: String +} + +type Contains { + _id: ID! +} + +type Route { + _id: ID! + dist: Int +} + +input RouteInput { + dist: Int +} + +input Options { + limit: Int +} + +type Query { + getAirport(code: String): Airport + getAirportConnection(fromCode: String!, toCode: String!): Airport + getAirportWithGremlin(code: String): Airport + getContinentsWithGremlin: [Continent] + getCountriesCountGremlin: Int + getNodeContinent(filter: ContinentInput): Continent + getNodeContinents(filter: ContinentInput, options: Options): [Continent] + getNodeCountry(filter: CountryInput): Country + getNodeCountrys(filter: CountryInput, options: Options): [Country] + getNodeVersion(filter: VersionInput): Version + getNodeVersions(filter: VersionInput, options: Options): [Version] + getNodeAirport(filter: AirportInput): Airport + getNodeAirports(filter: AirportInput, options: Options): [Airport] +} + +type Mutation { + createAirport(input: AirportInput!): Airport + addRoute(fromAirportCode: String, toAirportCode: String, dist: Int): Route + deleteAirport(id: ID): Int + createNodeContinent(input: ContinentInput!): Continent + updateNodeContinent(input: ContinentInput!): Continent + deleteNodeContinent(_id: ID!): Boolean + createNodeCountry(input: CountryInput!): Country + updateNodeCountry(input: CountryInput!): Country + deleteNodeCountry(_id: ID!): Boolean + createNodeVersion(input: VersionInput!): Version + updateNodeVersion(input: VersionInput!): Version + deleteNodeVersion(_id: ID!): Boolean + createNodeAirport(input: AirportInput!): Airport + updateNodeAirport(input: AirportInput!): Airport + deleteNodeAirport(_id: ID!): Boolean + connectNodeContinentToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains + deleteEdgeContainsFromContinentToAirport(from_id: ID!, to_id: ID!): Boolean + connectNodeCountryToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains + deleteEdgeContainsFromCountryToAirport(from_id: ID!, to_id: ID!): Boolean + connectNodeAirportToNodeAirportEdgeRoute(from_id: ID!, to_id: ID!, edge: RouteInput!): Route + updateEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!, edge: RouteInput!): Route + deleteEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!): Boolean +} + +schema { + query: Query + mutation: Mutation +} \ No newline at end of file diff --git a/test/TestCases/Case07/outputReference/output.source.schema.graphql b/test/TestCases/Case07/outputReference/output.source.schema.graphql new file mode 100644 index 0000000..21ebf4b --- /dev/null +++ b/test/TestCases/Case07/outputReference/output.source.schema.graphql @@ -0,0 +1,151 @@ +type Continent @alias(property: "continent") { + id: ID! @id + code: String + type: String + desc: String + airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) + contains: Contains +} + +input ContinentInput { + id: ID @id + code: String + type: String + desc: String +} + +type Country @alias(property: "country") { + _id: ID! @id + code: String + type: String + desc: String + airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) + contains: Contains +} + +input CountryInput { + _id: ID @id + code: String + type: String + desc: String +} + +type Version @alias(property: "version") { + _id: ID! @id + date: String + code: String + author: String + type: String + desc: String +} + +input VersionInput { + _id: ID @id + date: String + code: String + author: String + type: String + desc: String +} + +type Airport @alias(property: "airport") { + _id: ID! @id + country: String + longest: Float + code: String + city: String + elev: Float + icao: String + lon: Float + runways: Float + region: String + type: String + lat: Float + desc2: String @alias(property: "desc") + outboundRoutesCount: Int @graphQuery(statement: "MATCH (this)-[r:route]->(a) RETURN count(r)") + continentContainsIn: Continent @relationship(edgeType: "contains", direction: IN) + countryContainsIn: Country @relationship(edgeType: "contains", direction: IN) + airportRoutesOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: OUT) + airportRoutesIn(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: IN) + contains: Contains + route: Route +} + +input AirportInput { + _id: ID @id + country: String + longest: Float + code: String + city: String + elev: Float + icao: String + lon: Float + runways: Float + region: String + type: String + lat: Float + desc: String +} + +type Contains @alias(property: "contains") { + _id: ID! @id +} + +type Route @alias(property: "route") { + _id: ID! @id + dist: Int +} + +input RouteInput { + dist: Int +} + +input Options { + limit: Int +} + +type Query { + getAirport(code: String): Airport + getAirportConnection(fromCode: String!, toCode: String!): Airport @cypher(statement: "MATCH (:airport{code: '$fromCode'})-[:route]->(this:airport)-[:route]->(:airport{code:'$toCode'})") + getAirportWithGremlin(code: String): Airport @graphQuery(statement: "g.V().has('airport', 'code', '$code').elementMap()") + getContinentsWithGremlin: [Continent] @graphQuery(statement: "g.V().hasLabel('continent').elementMap().fold()") + getCountriesCountGremlin: Int @graphQuery(statement: "g.V().hasLabel('country').count()") + getNodeContinent(filter: ContinentInput): Continent + getNodeContinents(filter: ContinentInput, options: Options): [Continent] + getNodeCountry(filter: CountryInput): Country + getNodeCountrys(filter: CountryInput, options: Options): [Country] + getNodeVersion(filter: VersionInput): Version + getNodeVersions(filter: VersionInput, options: Options): [Version] + getNodeAirport(filter: AirportInput): Airport + getNodeAirports(filter: AirportInput, options: Options): [Airport] +} + +type Mutation { + createAirport(input: AirportInput!): Airport @graphQuery(statement: "CREATE (this:airport {$input}) RETURN this") + addRoute(fromAirportCode: String, toAirportCode: String, dist: Int): Route @graphQuery(statement: "MATCH (from:airport{code:'$fromAirportCode'}), (to:airport{code:'$toAirportCode'}) CREATE (from)-[this:route{dist:$dist}]->(to) RETURN this") + deleteAirport(id: ID): Int @graphQuery(statement: "MATCH (this:airport) WHERE ID(this) = '$id' DETACH DELETE this") + createNodeContinent(input: ContinentInput!): Continent + updateNodeContinent(input: ContinentInput!): Continent + deleteNodeContinent(_id: ID!): Boolean + createNodeCountry(input: CountryInput!): Country + updateNodeCountry(input: CountryInput!): Country + deleteNodeCountry(_id: ID!): Boolean + createNodeVersion(input: VersionInput!): Version + updateNodeVersion(input: VersionInput!): Version + deleteNodeVersion(_id: ID!): Boolean + createNodeAirport(input: AirportInput!): Airport + updateNodeAirport(input: AirportInput!): Airport + deleteNodeAirport(_id: ID!): Boolean + connectNodeContinentToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains + deleteEdgeContainsFromContinentToAirport(from_id: ID!, to_id: ID!): Boolean + connectNodeCountryToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains + deleteEdgeContainsFromCountryToAirport(from_id: ID!, to_id: ID!): Boolean + connectNodeAirportToNodeAirportEdgeRoute(from_id: ID!, to_id: ID!, edge: RouteInput!): Route + updateEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!, edge: RouteInput!): Route + deleteEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!): Boolean +} + +schema { + query: Query + mutation: Mutation +} \ No newline at end of file diff --git a/test/package.json b/test/package.json index e597aea..0db0fb5 100644 --- a/test/package.json +++ b/test/package.json @@ -10,6 +10,7 @@ "license": "ISC", "type": "module", "dependencies": { - "graphql-tag": "2.12.6" + "graphql-tag": "2.12.6", + "adm-zip": "0.5.16" } } diff --git a/test/testLib.js b/test/testLib.js index bb6a47d..9d6407d 100644 --- a/test/testLib.js +++ b/test/testLib.js @@ -1,6 +1,7 @@ import axios from 'axios'; import fs from 'fs'; +import AdmZip from 'adm-zip'; const HOST_PLACEHOLDER = ''; const PORT_PLACEHOLDER = ''; @@ -70,6 +71,19 @@ function checkOutputFilesContent(outputFolder, files, referenceFolder) { }); } +/** + * Unzips the given zip file and checks that the lambda uses the aws sdk + */ +function checkOutputZipLambdaUsesSdk(outputFolder, zipFile) { + const zip = new AdmZip(zipFile); + const lambdaFile = 'index.mjs'; + zip.extractEntryTo(lambdaFile, outputFolder + '/unzip', true, true); + + const lambdaContent = fs.readFileSync(outputFolder + '/unzip/' + lambdaFile, 'utf8'); + test('Lambda uses SDK: ' + lambdaFile, async () => { + expect(lambdaContent).toContain('@aws-sdk/client-neptune') + }); +} async function loadResolver(file) { return await import(file); @@ -126,4 +140,4 @@ async function testResolverQueriesResults(resolverFile, queriesReferenceFolder, } -export { readJSONFile, checkOutputFilesSize, checkOutputFilesContent, testResolverQueries, testResolverQueriesResults }; +export { readJSONFile, checkOutputFilesSize, checkOutputFilesContent, testResolverQueries, testResolverQueriesResults, checkOutputZipLambdaUsesSdk }; From 9d19f04ce66337a7a7935a52a2d667ce4affb551 Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Tue, 8 Oct 2024 21:58:52 -0700 Subject: [PATCH 06/11] Fixed packaging to include the new analytics sdk template. --- aws-neptune-for-graphql-1.1.0.tgz | Bin 0 -> 44296 bytes package.json | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 aws-neptune-for-graphql-1.1.0.tgz diff --git a/aws-neptune-for-graphql-1.1.0.tgz b/aws-neptune-for-graphql-1.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..b8f57ed014ce9aa31e61128233234715c97ba9c1 GIT binary patch literal 44296 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 Date: Tue, 8 Oct 2024 22:27:28 -0700 Subject: [PATCH 07/11] Fixed lambda environment variables to include neptune type which is required for axios credential interceptor in the http lambda. --- aws-neptune-for-graphql-1.1.0.tgz | Bin 44296 -> 44349 bytes src/CDKPipelineApp.js | 3 +++ src/pipelineResources.js | 1 + 3 files changed, 4 insertions(+) diff --git a/aws-neptune-for-graphql-1.1.0.tgz b/aws-neptune-for-graphql-1.1.0.tgz index b8f57ed014ce9aa31e61128233234715c97ba9c1..ff26f02c75de04d202f0e33b63b7b292d6572b7b 100644 GIT binary patch delta 42718 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⋘B7{?i<1Gn@hd5jX>76&ZPPJlbixGZspX-ZHPY4 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 delta 42660 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 diff --git a/src/CDKPipelineApp.js b/src/CDKPipelineApp.js index bb4f0fe..d916e0b 100644 --- a/src/CDKPipelineApp.js +++ b/src/CDKPipelineApp.js @@ -23,6 +23,7 @@ let REGION = ''; let NEPTUNE_DB_NAME = ''; let NEPTUNE_HOST = null; let NEPTUNE_PORT = null; +let NEPTUNE_TYPE = null; let NEPTUNE_DBSubnetGroup = null; let NEPTUNE_IAM_POLICY_RESOURCE = '*'; let LAMBDA_ZIP_FILE = ''; @@ -93,6 +94,7 @@ async function createAWSpipelineCDK({ NAME = pipelineName; REGION = neptuneDBregion; NEPTUNE_DB_NAME = neptuneDBName; + NEPTUNE_TYPE = neptuneType; APPSYNC_SCHEMA = appSyncSchema; SCHEMA_MODEL = schemaModel; NEPTUNE_HOST = neptuneHost; @@ -166,6 +168,7 @@ async function createAWSpipelineCDK({ CDKFile = CDKFile.replace( "const NEPTUNE_HOST = '';", `const NEPTUNE_HOST = '${NEPTUNE_HOST}';` ); CDKFile = CDKFile.replace( "const NEPTUNE_PORT = '';", `const NEPTUNE_PORT = '${NEPTUNE_PORT}';` ); CDKFile = CDKFile.replace( "const NEPTUNE_DB_NAME = '';", `const NEPTUNE_DB_NAME = '${NEPTUNE_DB_NAME}';` ); + CDKFile = CDKFile.replace( "const NEPTUNE_TYPE = '';", `const NEPTUNE_TYPE = '${NEPTUNE_TYPE}';` ); CDKFile = CDKFile.replace( "const NEPTUNE_DBSubnetGroup = null;", `const NEPTUNE_DBSubnetGroup = '${NEPTUNE_DBSubnetGroup}';` ); CDKFile = CDKFile.replace( "const NEPTUNE_IAM_AUTH = false;", `const NEPTUNE_IAM_AUTH = ${isNeptuneIAMAuth};` ); CDKFile = CDKFile.replace( "const NEPTUNE_IAM_POLICY_RESOURCE = '*';", `const NEPTUNE_IAM_POLICY_RESOURCE = '${NEPTUNE_IAM_POLICY_RESOURCE}';` ); diff --git a/src/pipelineResources.js b/src/pipelineResources.js index fee2b97..c73bcd3 100644 --- a/src/pipelineResources.js +++ b/src/pipelineResources.js @@ -344,6 +344,7 @@ async function createLambdaFunction() { "NEPTUNE_DB_NAME": NEPTUNE_DB_NAME, "NEPTUNE_REGION": REGION, "NEPTUNE_DOMAIN": parseNeptuneDomain(NEPTUNE_HOST), + "NEPTUNE_TYPE": NEPTUNE_TYPE, }, }, }; From b38cbf6f5d863d25cf2dce0e422bd42925cb5c42 Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Sun, 13 Oct 2024 22:58:39 -0700 Subject: [PATCH 08/11] Delete DS_STORE files committed by accident. --- test/TestCases/.DS_Store | Bin 6148 -> 0 bytes test/TestCases/Case07/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 test/TestCases/.DS_Store delete mode 100644 test/TestCases/Case07/.DS_Store diff --git a/test/TestCases/.DS_Store b/test/TestCases/.DS_Store deleted file mode 100644 index b5d81508a564ac05708ef3b15239364c882d559c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyN&`e4733uB$_R+%qKua8*%Uj`vZq`CmIA22Wg9eQyXA;Mg zC{wK0BBIOl_gthAkqO*Tt~T_|_RV`X$cO^rIAbP@OMBe!4*P8{`*FayWBHVgoaE;Z z-}Y!!fC^9nDnJFOz;hMI`Z}3D_gJ1r1*pIqC}7`*0ynIQU7&wDFn9|993kw6x%U#l zVgX=H>;e&iX;6Vd)od{|=!lohtBGA;&_%QP(7ai*Ls7pS=NC^Gt$`e=02R1bU>M8B z>i-)4Pyc^U;))7TfwxjXN2_+V#FMhN_8w=ow!pvOmh%fY!`vwtyc`3)9AjbS_`{PT auh<;>HL(kHI^s?T@@K$wp;3WXD{uqNbrg{R diff --git a/test/TestCases/Case07/.DS_Store b/test/TestCases/Case07/.DS_Store deleted file mode 100644 index 1b83a3bed89e754471532e37c85dffb2fb73f08e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKF>V4u473vzA<-Fqv zr#PR@%ooS)huPfBrf{O2IgE|_^pU+(#DQ>}akQV)`g{L**zZQ!e+S5Yvjds-b;IYL zObSQ=DIf);fE0MF0##q9v&SB*)1-hDcmf6ZeQ0oEFB}r%(}5vc0N@1SFwCQu05%4I zy>LiG1m;NvCe^FO@T4Q&Dz6s~iAguF=ELh|uMWlIcAVcL-MlAilmb%VT7lPG&RPFg z@H_qgHAyQeAO)UE0iP__%LSfPwRQA3*4hT&z?t(4r(qrx3{j4OQI4@-Iew3%%xj!u WzZVXPK}S63K>Z9*7nv0JZv`%`G!@|h From e487e9c9216afbc6694a743235a83e98e3126612 Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Mon, 14 Oct 2024 22:59:23 -0700 Subject: [PATCH 09/11] Changed gitignore to use patterns. Removed binary tarball. Changed console error messages to reference neptune-type. Changed logic which determines neptune-type from endpoint to examine the host only. --- .gitignore | 16 ++------------- aws-neptune-for-graphql-1.1.0.tgz | Bin 44349 -> 0 bytes src/NeptuneSchema.js | 17 ++++++++-------- src/main.js | 8 ++++---- src/pipelineResources.js | 4 ++-- src/test/util.test.js | 22 ++++++++++++++++----- src/util.js | 31 ++++++++++++++++++++++++------ templates/CDKTemplate.js | 4 ++-- 8 files changed, 61 insertions(+), 41 deletions(-) delete mode 100644 aws-neptune-for-graphql-1.1.0.tgz diff --git a/.gitignore b/.gitignore index 09595a7..da73e42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,6 @@ -output/** package-lock.json .vscode/launch.json -node_modules/** -templates/Lambda4AppSyncHTTP/node_modules/** -templates/Lambda4AppSyncSDK/node_modules/** -templates/Lambda4AppSyncGraphSDK/node_modules/** +**/node_modules/ coverage/** -test/node_modules/** -test/TestCases/Case01/output/** -test/TestCases/Case01/output/** -test/TestCases/Case02/output/** -test/TestCases/Case03/output/** -test/TestCases/Case04/output/** -test/TestCases/Case05/output/** -test/TestCases/Case06/output/** -test/TestCases/Case07/output/** +**/output/ *.iml diff --git a/aws-neptune-for-graphql-1.1.0.tgz b/aws-neptune-for-graphql-1.1.0.tgz deleted file mode 100644 index ff26f02c75de04d202f0e33b63b7b292d6572b7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44349 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 N{}0I2cZvY60|2ljXEp!; diff --git a/src/NeptuneSchema.js b/src/NeptuneSchema.js index f7350d2..8380691 100644 --- a/src/NeptuneSchema.js +++ b/src/NeptuneSchema.js @@ -14,10 +14,11 @@ import axios from "axios"; import { aws4Interceptor } from "aws4-axios"; import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; import { NeptunedataClient, ExecuteOpenCypherQueryCommand } from "@aws-sdk/client-neptunedata"; -import { parseNeptuneDomain, parseNeptuneGraphName } from "./util.js"; +import { parseNeptuneDomainFromHost, parseNeptuneGraphName } from "./util.js"; import { ExecuteQueryCommand, GetGraphSummaryCommand, NeptuneGraphClient } from "@aws-sdk/client-neptune-graph"; const NEPTUNE_DB = 'neptune-db'; +const NEPTUNE_GRAPH = 'neptune-graph'; const NEPTUNE_GRAPH_PROTOCOL = 'https'; const HTTP_LANGUAGE = 'openCypher'; const NEPTUNE_GRAPH_LANGUAGE = 'OPEN_CYPHER'; @@ -120,7 +121,7 @@ async function queryNeptuneDbSDK(query, params = '{}') { return response; } catch (error) { - console.error("Neptune db SDK query request failed: ", error.message); + console.error(NEPTUNE_DB + ' SDK query request failed: ', error.message); process.exit(1); } } @@ -140,7 +141,7 @@ async function queryNeptuneGraphSDK(query, params = '{}') { const response = await client.send(command); return await new Response(response.payload).json(); } catch (error) { - console.error('Neptune graph SDK query request failed:' + JSON.stringify(error)); + console.error(NEPTUNE_GRAPH + ' SDK query request failed:' + JSON.stringify(error)); process.exit(1); } } @@ -350,7 +351,7 @@ function getNeptuneGraphClient() { console.log('Instantiating NeptuneGraphClient') neptuneGraphClient = new NeptuneGraphClient({ port: PORT, - host: parseNeptuneDomain(HOST), + host: parseNeptuneDomainFromHost(HOST), region: REGION, protocol: NEPTUNE_GRAPH_PROTOCOL, }); @@ -362,14 +363,14 @@ function getNeptuneGraphClient() { * Get a summary of a neptune analytics graph */ async function getNeptuneGraphSummary() { - console.log('Retrieving neptune graph summary') + console.log('Retrieving ' + NEPTUNE_GRAPH + ' summary') const client = getNeptuneGraphClient(); const command = new GetGraphSummaryCommand({ graphIdentifier: NAME, mode: 'detailed' }); const response = await client.send(command); - console.log('Retrieved neptune graph summary') + console.log('Retrieved ' + NEPTUNE_GRAPH + ' summary') return response.graphSummary; } @@ -377,13 +378,13 @@ async function getNeptuneGraphSummary() { * Get a summary of a neptune db graph */ async function getNeptuneDbSummary() { - console.log('Retrieving neptune db summary') + console.log('Retrieving ' + NEPTUNE_DB + ' summary') let response = await axios.get(`https://${HOST}:${PORT}/propertygraph/statistics/summary`, { params: { mode: 'detailed' } }); - console.log('Retrieved neptune db summary') + console.log('Retrieved ' + NEPTUNE_DB + ' summary') return response.data.payload.graphSummary; } diff --git a/src/main.js b/src/main.js index 06cb5a5..c657996 100644 --- a/src/main.js +++ b/src/main.js @@ -31,6 +31,7 @@ function yellow(text) { // find global installation dir import path from 'path'; import { fileURLToPath } from 'url'; +import { parseNeptuneDomainFromEndpoint } from "./util.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -279,9 +280,8 @@ async function main() { } // Check if Neptune target is db or graph - if (inputGraphDBSchemaNeptuneEndpoint.includes(NEPTUNE_GRAPH) || - createUpdatePipelineEndpoint.includes(NEPTUNE_GRAPH) || - inputCDKpipelineEnpoint.includes(NEPTUNE_GRAPH)) { + const nonEmptyEndpoints = [inputGraphDBSchemaNeptuneEndpoint, createUpdatePipelineEndpoint, inputCDKpipelineEnpoint].filter(endpoint => endpoint !== ''); + if (nonEmptyEndpoints.length > 0 && parseNeptuneDomainFromEndpoint(nonEmptyEndpoints[0]).includes(NEPTUNE_GRAPH)) { neptuneType = NEPTUNE_GRAPH; // neptune analytics requires IAM console.log("Detected neptune-graph from input endpoint - setting IAM auth to true as it is required for neptune analytics") @@ -306,7 +306,7 @@ async function main() { neptuneRegion = neptuneRegionParts[1]; if (!quiet) console.log('Getting Neptune schema from endpoint: ' + yellow(neptuneHost + ':' + neptunePort)); - setGetNeptuneSchemaParameters(neptuneHost, neptunePort, neptuneRegion, true); + setGetNeptuneSchemaParameters(neptuneHost, neptunePort, neptuneRegion, true, neptuneType); let startTime = performance.now(); inputGraphDBSchema = await getNeptuneSchema(quiet); let endTime = performance.now(); diff --git a/src/pipelineResources.js b/src/pipelineResources.js index c73bcd3..dec3b0d 100644 --- a/src/pipelineResources.js +++ b/src/pipelineResources.js @@ -47,7 +47,7 @@ import fs from 'fs'; import archiver from 'archiver'; import ora from 'ora'; import { exit } from "process"; -import { parseNeptuneDomain } from "./util.js"; +import { parseNeptuneDomainFromHost } from "./util.js"; const NEPTUNE_DB = 'neptune-db'; @@ -343,7 +343,7 @@ async function createLambdaFunction() { "LOGGING_ENABLED": "false", "NEPTUNE_DB_NAME": NEPTUNE_DB_NAME, "NEPTUNE_REGION": REGION, - "NEPTUNE_DOMAIN": parseNeptuneDomain(NEPTUNE_HOST), + "NEPTUNE_DOMAIN": parseNeptuneDomainFromHost(NEPTUNE_HOST), "NEPTUNE_TYPE": NEPTUNE_TYPE, }, }, diff --git a/src/test/util.test.js b/src/test/util.test.js index 906ea71..9e10f6a 100644 --- a/src/test/util.test.js +++ b/src/test/util.test.js @@ -1,15 +1,27 @@ -import {parseNeptuneDomain, parseNeptuneGraphName} from '../util.js'; +import {parseNeptuneDomainFromEndpoint, parseNeptuneDomainFromHost, parseNeptuneGraphName} from '../util.js'; test('parse domain from neptune cluster host', () => { - expect(parseNeptuneDomain('db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com')).toBe('neptune.amazonaws.com'); + expect(parseNeptuneDomainFromHost('db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com')).toBe('neptune.amazonaws.com'); }); test('parse domain from neptune analytics host', () => { - expect(parseNeptuneDomain('g-abcdef.us-west-2.neptune-graph.amazonaws.com')).toBe('neptune-graph.amazonaws.com'); + expect(parseNeptuneDomainFromHost('g-abcdef.us-west-2.neptune-graph.amazonaws.com')).toBe('neptune-graph.amazonaws.com'); }); test('parse domain from host without enough parts throws error', () => { - expect(() => parseNeptuneDomain('invalid.com')).toThrow('Cannot parse neptune host invalid.com because it has 2 parts but expected at least 5'); + expect(() => parseNeptuneDomainFromHost('invalid.com')).toThrow('Cannot parse neptune host invalid.com because it has 2 part(s) delimited by . but expected at least 5'); +}); + +test('parse domain from neptune cluster endpoint', () => { + expect(parseNeptuneDomainFromEndpoint('db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com:8182')).toBe('neptune.amazonaws.com'); +}); + +test('parse domain from neptune analytics endpoint', () => { + expect(parseNeptuneDomainFromEndpoint('g-abcdef.us-west-2.neptune-graph.amazonaws.com:8182')).toBe('neptune-graph.amazonaws.com'); +}); + +test('parse domain from endpoint without enough parts throws error', () => { + expect(() => parseNeptuneDomainFromEndpoint('g-abcdef.us-west-2.neptune-graph.amazonaws.com')).toThrow('Cannot parse domain from neptune endpoint g-abcdef.us-west-2.neptune-graph.amazonaws.com because it has 1 part(s) delimited by : but expected 2'); }); test('parse name from neptune cluster host', () => { @@ -21,5 +33,5 @@ test('parse name from neptune analytics host', () => { }); test('parse name from host without enough parts throws error', () => { - expect(() => parseNeptuneGraphName('invalid.com')).toThrow('Cannot parse neptune host invalid.com because it has 2 parts but expected at least 5'); + expect(() => parseNeptuneGraphName('invalid.com')).toThrow('Cannot parse neptune host invalid.com because it has 2 part(s) delimited by . but expected at least 5'); }); \ No newline at end of file diff --git a/src/util.js b/src/util.js index 31ea3b3..600a17c 100644 --- a/src/util.js +++ b/src/util.js @@ -12,7 +12,8 @@ permissions and limitations under the License. const MIN_HOST_PARTS = 5; const NUM_DOMAIN_PARTS = 3; -const DELIMITER = '.'; +const HOST_DELIMITER = '.'; +const ENDPOINT_DELIMITER = ':'; /** * Splits a neptune host into its parts, throwing an Error if there are unexpected number of parts. @@ -20,9 +21,10 @@ const DELIMITER = '.'; * @param neptuneHost */ function splitHost(neptuneHost) { - let parts = neptuneHost.split(DELIMITER); + let parts = neptuneHost.split(HOST_DELIMITER); if (parts.length < MIN_HOST_PARTS) { - throw Error('Cannot parse neptune host ' + neptuneHost + ' because it has ' + parts.length + ' parts but expected at least ' + MIN_HOST_PARTS); + throw Error('Cannot parse neptune host ' + neptuneHost + ' because it has ' + parts.length + + ' part(s) delimited by ' + HOST_DELIMITER + ' but expected at least ' + MIN_HOST_PARTS); } return parts; } @@ -35,12 +37,29 @@ function splitHost(neptuneHost) { * * @param neptuneHost */ -function parseNeptuneDomain(neptuneHost) { +function parseNeptuneDomainFromHost(neptuneHost) { let parts = splitHost(neptuneHost); // last 3 parts of the host make up the domain // ie. neptune.amazonaws.com or neptune-graph.amazonaws.com let domainParts = parts.splice(parts.length - NUM_DOMAIN_PARTS, NUM_DOMAIN_PARTS); - return domainParts.join(DELIMITER); + return domainParts.join(HOST_DELIMITER); +} + +/** + * Parses the domain from the given neptune db or neptune analytics endpoint. + * + * Example: g-abcdef.us-west-2.neptune-graph.amazonaws.com:8182 ==> neptune-graph.amazonaws.com + * Example: db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com:8182 ==> neptune.amazonaws.com + * + * @param neptuneEndpoint + */ +function parseNeptuneDomainFromEndpoint(neptuneEndpoint) { + let parts = neptuneEndpoint.split(ENDPOINT_DELIMITER); + if (parts.length !== 2) { + throw Error('Cannot parse domain from neptune endpoint ' + neptuneEndpoint + ' because it has ' + + parts.length + ' part(s) delimited by ' + ENDPOINT_DELIMITER + ' but expected 2'); + } + return parseNeptuneDomainFromHost(parts[0]); } /** @@ -57,4 +76,4 @@ function parseNeptuneGraphName(neptuneHost) { return parts[0]; } -export {parseNeptuneDomain, parseNeptuneGraphName}; \ No newline at end of file +export {parseNeptuneDomainFromHost, parseNeptuneDomainFromEndpoint, parseNeptuneGraphName}; \ No newline at end of file diff --git a/templates/CDKTemplate.js b/templates/CDKTemplate.js index ce9d0a9..caf016c 100644 --- a/templates/CDKTemplate.js +++ b/templates/CDKTemplate.js @@ -14,7 +14,7 @@ const { Stack, Duration, App } = require('aws-cdk-lib'); const lambda = require( 'aws-cdk-lib/aws-lambda'); const iam = require( 'aws-cdk-lib/aws-iam'); const ec2 = require( 'aws-cdk-lib/aws-ec2'); -const { parseNeptuneDomain } = require('../src/util.js'); +const { parseNeptuneDomainFromHost } = require('../src/util.js'); const { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema, CfnDataSource, CfnResolver, CfnFunctionConfiguration } = require( 'aws-cdk-lib/aws-appsync'); const NAME = ''; @@ -64,7 +64,7 @@ class AppSyncNeptuneStack extends Stack { LOGGING_ENABLED: 'false', NEPTUNE_DB_NAME: NEPTUNE_DB_NAME, NEPTUNE_REGION: REGION, - NEPTUNE_DOMAIN: parseNeptuneDomain(NEPTUNE_HOST), + NEPTUNE_DOMAIN: parseNeptuneDomainFromHost(NEPTUNE_HOST), NEPTUNE_TYPE: NEPTUNE_TYPE, }; if (NEPTUNE_IAM_AUTH) { From 876445512ddf5b6e9ff22def409528e808f81396 Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Mon, 14 Oct 2024 23:34:12 -0700 Subject: [PATCH 10/11] Moved output folder creation back down after the inputs have been processed so that it is not created if there are errors with the input. Changed lambda action param to be array of single item. Changed http lambda to use default service name of neptune-db if the environment variable is not set. --- src/main.js | 5 +++-- src/pipelineResources.js | 2 +- templates/Lambda4AppSyncHTTP/index.mjs | 9 ++++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main.js b/src/main.js index c657996..ca2f396 100644 --- a/src/main.js +++ b/src/main.js @@ -266,8 +266,6 @@ async function main() { processArgs(); - // Init output folder - mkdirSync(outputFolderPath, { recursive: true }); // Get graphDB schema from file if (inputGraphDBSchemaFile != '' && inputGraphQLSchema == '' && inputGraphQLSchemaFile == '') { try { @@ -433,6 +431,9 @@ async function main() { // Outputs // **************************************************************************** + // Init output folder + mkdirSync(outputFolderPath, { recursive: true }); + // Output GraphQL schema no directives if (inputGraphQLSchema != '') { diff --git a/src/pipelineResources.js b/src/pipelineResources.js index dec3b0d..fd8d672 100644 --- a/src/pipelineResources.js +++ b/src/pipelineResources.js @@ -252,7 +252,7 @@ async function createLambdaRole() { "neptune-db:WriteDataViaQuery" ]; } else { - action = "neptune-graph:*" + action = ["neptune-graph:*"] } // Create Neptune query policy diff --git a/templates/Lambda4AppSyncHTTP/index.mjs b/templates/Lambda4AppSyncHTTP/index.mjs index 06f6569..65fe8d9 100644 --- a/templates/Lambda4AppSyncHTTP/index.mjs +++ b/templates/Lambda4AppSyncHTTP/index.mjs @@ -14,10 +14,17 @@ const { if (process.env.NEPTUNE_IAM_AUTH_ENABLED === 'true') { + let serviceName; + if (process.env.NEPTUNE_TYPE) { + serviceName = process.env.NEPTUNE_TYPE; + } else { + console.log('NEPTUNE_TYPE environment variable is not set - defaulting to neptune-db'); + serviceName = 'neptune-db'; + } const interceptor = aws4Interceptor({ options: { region: AWS_REGION, - service: process.env.NEPTUNE_TYPE, + service: serviceName, }, credentials: { accessKeyId: AWS_ACCESS_KEY_ID, From 46d6c1f37770a40079c9b4083c1dd37a6b61e56c Mon Sep 17 00:00:00 2001 From: Andrea Child Date: Tue, 15 Oct 2024 10:47:52 -0700 Subject: [PATCH 11/11] Update neptune query via sdk functions to use empty object as default params. Added default region back and added logic to switch to parsed region from endpoint if they differ. Added some comments for clarity around parsing of neptune analytics graph type. Changed CDK template to inline function from util.js instead of require or import. Refactor node property and edge name collision to use a set and added unit test. --- src/NeptuneSchema.js | 8 ++-- src/graphdb.js | 30 +++++++-------- src/main.js | 24 ++++++++---- src/test/graphdb.test.js | 82 ++++++++++++++++++++++++++++++++++++++++ templates/CDKTemplate.js | 19 +++++++++- 5 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 src/test/graphdb.test.js diff --git a/src/NeptuneSchema.js b/src/NeptuneSchema.js index f600870..d50d145 100644 --- a/src/NeptuneSchema.js +++ b/src/NeptuneSchema.js @@ -79,7 +79,7 @@ function sanitize(text) { */ async function queryNeptune(query, params = {}) { if (useSDK) { - return await queryNeptuneSDK(query, params); + return await queryNeptuneSdk(query, params); } else { try { let data = { @@ -106,7 +106,7 @@ async function queryNeptune(query, params = {}) { /** * Queries neptune using an SDK. */ -async function queryNeptuneSdk(query, params = '{}') { +async function queryNeptuneSdk(query, params = {}) { if (NEPTUNE_TYPE === NEPTUNE_DB) { return await queryNeptuneDbSDK(query, params); } else { @@ -137,14 +137,14 @@ async function queryNeptuneDbSDK(query, params = {}) { /** * Queries neptune analytics graph using SDK (not to be used for neptune db). */ -async function queryNeptuneGraphSDK(query, params = '{}') { +async function queryNeptuneGraphSDK(query, params = {}) { try { const client = getNeptuneGraphClient(); const command = new ExecuteQueryCommand({ graphIdentifier: NAME, queryString: query, language: NEPTUNE_GRAPH_LANGUAGE, - parameters: JSON.parse(params) + parameters: params }); const response = await client.send(command); return await new Response(response.payload).json(); diff --git a/src/graphdb.js b/src/graphdb.js index 9065db1..c0ea668 100644 --- a/src/graphdb.js +++ b/src/graphdb.js @@ -69,16 +69,12 @@ function graphDBInferenceSchema (graphbSchema, addMutations) { } r += '\t_id: ID! @id\n'; - - let properties = []; - node.properties.forEach(property => { - properties.push(property.name); + node.properties.forEach(property => { if (property.name == 'id') r+= `\tid: ID\n`; else r+= `\t${property.name}: ${property.type}\n`; - }); let edgeTypes = []; @@ -130,17 +126,21 @@ function graphDBInferenceSchema (graphbSchema, addMutations) { }); }); - // Add edge types - edgeTypes.forEach(edgeType => { - let collision = ''; - if (properties.includes(edgeType)) - collision = '_'; + const nodePropertyNames = new Set(node.properties.map((p) => p.name)); - if (changeCase) { - r += `\t${collision + edgeType}:${toPascalCase(edgeType)}` - } else { - r += `\t${collision + edgeType}:${edgeType}` - } + // Add edge types + edgeTypes.forEach((edgeType) => { + // resolve any collision with node properties with the same name by adding an underscore prefix + const aliasedEdgeType = nodePropertyNames.has(edgeType) + ? `_${edgeType}` + : edgeType; + + // Modify the case if configured + const caseAdjustedEdgeType = changeCase + ? toPascalCase(edgeType) + : edgeType; + + r += `\t${aliasedEdgeType}:${caseAdjustedEdgeType}`; }); r += '}\n\n'; diff --git a/src/main.js b/src/main.js index fee4fe5..01c6af2 100644 --- a/src/main.js +++ b/src/main.js @@ -59,7 +59,7 @@ let isNeptuneIAMAuth = false; let createUpdatePipeline = false; let createUpdatePipelineName = ''; let createUpdatePipelineEndpoint = ''; -let createUpdatePipelineRegion = ''; +let createUpdatePipelineRegion = 'us-east-1'; let createUpdatePipelineNeptuneDatabaseName = ''; let removePipelineName = ''; let inputCDKpipeline = false; @@ -277,9 +277,10 @@ async function main() { } } - // Check if Neptune target is db or graph + // Check if any of the Neptune endpoints are a neptune analytic endpoint and if so, set the neptuneType and IAM to required const nonEmptyEndpoints = [inputGraphDBSchemaNeptuneEndpoint, createUpdatePipelineEndpoint, inputCDKpipelineEnpoint].filter(endpoint => endpoint !== ''); - if (nonEmptyEndpoints.length > 0 && parseNeptuneDomainFromEndpoint(nonEmptyEndpoints[0]).includes(NEPTUNE_GRAPH)) { + const isNeptuneAnalyticsGraph = nonEmptyEndpoints.length > 0 && parseNeptuneDomainFromEndpoint(nonEmptyEndpoints[0]).includes(NEPTUNE_GRAPH); + if (isNeptuneAnalyticsGraph) { neptuneType = NEPTUNE_GRAPH; // neptune analytics requires IAM console.log("Detected neptune-graph from input endpoint - setting IAM auth to true as it is required for neptune analytics") @@ -365,12 +366,21 @@ async function main() { if (createUpdatePipelineEndpoint != '') { let parts = createUpdatePipelineEndpoint.split('.'); createUpdatePipelineNeptuneDatabaseName = parts[0]; - if (createUpdatePipelineRegion === '') { - if (neptuneType === NEPTUNE_DB) { - createUpdatePipelineRegion = parts[2]; + + let parsedRegion; + if (neptuneType === NEPTUNE_DB) { + parsedRegion = parts[2]; + } else { + parsedRegion = parts[1]; + } + + if (createUpdatePipelineRegion !== parsedRegion) { + if (createUpdatePipelineRegion !== '') { + console.log('Switching region from ' + createUpdatePipelineRegion + ' to region parsed from endpoint: ' + parsedRegion); } else { - createUpdatePipelineRegion = parts[1]; + console.log('Region parsed from endpoint: ' + parsedRegion); } + createUpdatePipelineRegion = parsedRegion; } } if (createUpdatePipelineName == '') { diff --git a/src/test/graphdb.test.js b/src/test/graphdb.test.js new file mode 100644 index 0000000..3e4a1d3 --- /dev/null +++ b/src/test/graphdb.test.js @@ -0,0 +1,82 @@ +import {graphDBInferenceSchema} from '../graphdb.js'; + +const SCHEMA_WITH_PROPERTY_AND_EDGE_SAME_NAME = { + "nodeStructures": [ + { + "label": "continent", + "properties": [ + { + "name": "id", + "type": "String" + }, + { + "name": "code", + "type": "String" + }, + { + "name": "desc", + "type": "String" + }, + { + "name": "commonName", + "type": "String" + } + ] + }, + { + "label": "country", + "properties": [ + { + "name": "id", + "type": "String" + }, + { + "name": "code", + "type": "String" + }, + { + "name": "desc", + "type": "String" + } + ] + } + ], + "edgeStructures": [ + { + "label": "contains", + "properties": [ + { + "name": "id", + "type": "String" + } + ], + "directions": [ + { + "from": "continent", + "to": "country", + "relationship": "ONE-MANY" + } + ] + }, + { + "label": "commonName", + "properties": [ + { + "name": "id", + "type": "String" + } + ], + "directions": [ + { + "from": "continent", + "to": "country", + "relationship": "ONE-MANY" + } + ] + } + ] +}; + +test('node with same property and edge label should add underscore prefix', () => { + expect(graphDBInferenceSchema(JSON.stringify(SCHEMA_WITH_PROPERTY_AND_EDGE_SAME_NAME), false)).toContain('_commonName:Commonname'); +}); diff --git a/templates/CDKTemplate.js b/templates/CDKTemplate.js index caf016c..c6efa34 100644 --- a/templates/CDKTemplate.js +++ b/templates/CDKTemplate.js @@ -14,7 +14,6 @@ const { Stack, Duration, App } = require('aws-cdk-lib'); const lambda = require( 'aws-cdk-lib/aws-lambda'); const iam = require( 'aws-cdk-lib/aws-iam'); const ec2 = require( 'aws-cdk-lib/aws-ec2'); -const { parseNeptuneDomainFromHost } = require('../src/util.js'); const { CfnGraphQLApi, CfnApiKey, CfnGraphQLSchema, CfnDataSource, CfnResolver, CfnFunctionConfiguration } = require( 'aws-cdk-lib/aws-appsync'); const NAME = ''; @@ -37,6 +36,10 @@ const APPSYNC_ATTACH_QUERY = []; const APPSYNC_ATTACH_MUTATION = []; +const MIN_HOST_PARTS = 5; +const NUM_DOMAIN_PARTS = 3; +const HOST_DELIMITER = '.'; + class AppSyncNeptuneStack extends Stack { /** * @@ -64,7 +67,7 @@ class AppSyncNeptuneStack extends Stack { LOGGING_ENABLED: 'false', NEPTUNE_DB_NAME: NEPTUNE_DB_NAME, NEPTUNE_REGION: REGION, - NEPTUNE_DOMAIN: parseNeptuneDomainFromHost(NEPTUNE_HOST), + NEPTUNE_DOMAIN: this.parseNeptuneDomainFromHost(NEPTUNE_HOST), NEPTUNE_TYPE: NEPTUNE_TYPE, }; if (NEPTUNE_IAM_AUTH) { @@ -262,6 +265,18 @@ export function response(ctx) { } + + parseNeptuneDomainFromHost(neptuneHost) { + let parts = neptuneHost.split(HOST_DELIMITER); + if (parts.length < MIN_HOST_PARTS) { + throw Error('Cannot parse neptune host ' + neptuneHost + ' because it has ' + parts.length + + ' part(s) delimited by ' + HOST_DELIMITER + ' but expected at least ' + MIN_HOST_PARTS); + } + // last 3 parts of the host make up the domain + // ie. neptune.amazonaws.com or neptune-graph.amazonaws.com + let domainParts = parts.splice(parts.length - NUM_DOMAIN_PARTS, NUM_DOMAIN_PARTS); + return domainParts.join(HOST_DELIMITER); + } }