From e0656aeec3a8ba37bd03a7f5ed6b0f556cfbf5bf Mon Sep 17 00:00:00 2001 From: andreachild Date: Tue, 15 Oct 2024 15:54:07 -0700 Subject: [PATCH] Integration with Neptune Analytics (#31) Added support for integration with Neptune Analytics via SDK or http: added new dependency on client-neptune-graph 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) 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 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 | 14 +- package.json | 8 +- src/CDKPipelineApp.js | 105 +- src/NeptuneSchema.js | 162 +- src/graphdb.js | 22 +- src/main.js | 115 +- src/pipelineResources.js | 275 +- src/test/graphdb.test.js | 82 + src/test/util.test.js | 37 + src/util.js | 79 + templates/CDKTemplate.js | 44 +- templates/Lambda4AppSyncGraphSDK/index.mjs | 93 + templates/Lambda4AppSyncGraphSDK/package.json | 15 + templates/Lambda4AppSyncHTTP/index.mjs | 9 +- 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 +- 24 files changed, 5698 insertions(+), 278 deletions(-) create mode 100644 src/test/graphdb.test.js 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/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..da73e42 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,6 @@ -output/** package-lock.json .vscode/launch.json -node_modules/** -templates/Lambda4AppSyncHTTP/node_modules/** -templates/Lambda4AppSyncSDK/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/** +**/output/ *.iml diff --git a/package.json b/package.json index e22a98c..156a43d 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", @@ -19,9 +19,10 @@ "test": "test" }, "scripts": { - "postinstall": "cd templates/Lambda4AppSyncHTTP && npm install && cd ../Lambda4AppSyncSDK && npm install", + "postinstall": "cd templates/Lambda4AppSyncHTTP && npm install && cd ../Lambda4AppSyncSDK && npm install && cd ../Lambda4AppSyncGraphSDK && 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": { @@ -42,6 +43,8 @@ "./templates/Lambda4AppSyncHTTP/package.json", "./templates/Lambda4AppSyncSDK/index.mjs", "./templates/Lambda4AppSyncSDK/package.json", + "./templates/Lambda4AppSyncGraphSDK/index.mjs", + "./templates/Lambda4AppSyncGraphSDK/package.json", "./src/**" ], "author": "AWS", @@ -53,6 +56,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..d916e0b 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'; @@ -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 = ''; @@ -73,11 +74,27 @@ 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; NEPTUNE_DB_NAME = neptuneDBName; + NEPTUNE_TYPE = neptuneType; APPSYNC_SCHEMA = appSyncSchema; SCHEMA_MODEL = schemaModel; NEPTUNE_HOST = neptuneHost; @@ -88,50 +105,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 +166,9 @@ 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_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/NeptuneSchema.js b/src/NeptuneSchema.js index 7970097..d50d145 100644 --- a/src/NeptuneSchema.js +++ b/src/NeptuneSchema.js @@ -14,15 +14,24 @@ 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 { 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'; let HOST = ''; let PORT = 8182; let REGION = '' let SAMPLE = 5000; -let VERBOSE = false; -let language = 'openCypher'; +let VERBOSE = false; +let NEPTUNE_TYPE = NEPTUNE_DB; +let NAME = ''; let useSDK = false; - +let neptuneGraphClient; +let neptunedataClient; async function getAWSCredentials() { const credentialProvider = fromNodeProviderChain(); @@ -31,7 +40,7 @@ async function getAWSCredentials() { const interceptor = aws4Interceptor({ options: { region: REGION, - service: "neptune-db", + service: NEPTUNE_TYPE, }, credentials: cred }); @@ -64,22 +73,20 @@ function sanitize(text) { } /** - * Executes a neptune query + * 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(query, params); - return response; + return await queryNeptuneSdk(query, params); } else { try { let data = { query: query, parameters: JSON.stringify(params) }; - const response = await axios.post(`https://${HOST}:${PORT}/${language}`, data, { + const response = await axios.post(`https://${HOST}:${PORT}/${HTTP_LANGUAGE}`, data, { headers: { 'Content-Type': 'application/json' } @@ -88,20 +95,31 @@ async function queryNeptune(query, params = {}) { } catch (error) { console.error("Http query request failed: ", error.message); consoleOut("Trying with the AWS SDK"); - const response = await queryNeptuneSDK(query, params); + 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(query, params = {}) { +/** + * 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: query, parameters: JSON.stringify(params) @@ -110,8 +128,28 @@ async function queryNeptuneSDK(query, params = {}) { 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: 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); } } @@ -119,7 +157,7 @@ async function queryNeptuneSDK(query, params = {}) { 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 => { @@ -299,23 +337,84 @@ 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; + 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: parseNeptuneDomainFromHost(HOST), + region: REGION, + protocol: NEPTUNE_GRAPH_PROTOCOL, + }); + } + return neptuneGraphClient; +} + +/** + * 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; +} -async function getSchemaViaSummaryAPI() { +/** + * 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)); }); @@ -323,25 +422,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/graphdb.js b/src/graphdb.js index 17f43ea..c0ea668 100644 --- a/src/graphdb.js +++ b/src/graphdb.js @@ -126,13 +126,21 @@ function graphDBInferenceSchema (graphbSchema, addMutations) { }); }); - // Add edge types - edgeTypes.forEach(edgeType => { - if (changeCase) { - r += `\t${edgeType}:${toPascalCase(edgeType)}` - } else { - r += `\t${edgeType}:${edgeType}` - } + const nodePropertyNames = new Set(node.properties.map((p) => p.name)); + + // 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 9838c4b..01c6af2 100644 --- a/src/main.js +++ b/src/main.js @@ -31,12 +31,19 @@ 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); // 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,7 +70,7 @@ let inputCDKpipelineRegion = ''; let inputCDKpipelineDatabaseName = ''; let createLambdaZip = true; let outputFolderPath = './output'; - +let neptuneType = NEPTUNE_DB; // or neptune-graph // Outputs let outputSchema = ''; @@ -247,6 +254,7 @@ function processArgs() { break; } }); + } async function main() { @@ -262,28 +270,42 @@ 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); } } + // 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 !== ''); + 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") + isNeptuneIAMAuth = true; + } + // 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); } 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); + setGetNeptuneSchemaParameters(neptuneHost, neptunePort, neptuneRegion, true, neptuneType); let startTime = performance.now(); inputGraphDBSchema = await getNeptuneSchema(quiet); let endTime = performance.now(); @@ -340,11 +362,26 @@ async function main() { createUpdatePipelineRegion == '' && !createUpdatePipelineNeptuneDatabaseName == '') { console.error('AWS pipeline: a Neptune database region is required.'); process.exit(1); - } + } if (createUpdatePipelineEndpoint != '') { let parts = createUpdatePipelineEndpoint.split('.'); createUpdatePipelineNeptuneDatabaseName = parts[0]; - 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 { + console.log('Region parsed from endpoint: ' + parsedRegion); + } + createUpdatePipelineRegion = parsedRegion; + } } if (createUpdatePipelineName == '') { createUpdatePipelineName = createUpdatePipelineNeptuneDatabaseName; @@ -404,6 +441,7 @@ async function main() { // Outputs // **************************************************************************** + // Init output folder mkdirSync(outputFolderPath, { recursive: true }); // Output GraphQL schema no directives @@ -422,7 +460,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); } @@ -440,7 +478,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); } @@ -457,7 +495,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); } @@ -470,7 +508,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); } @@ -485,10 +523,9 @@ 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); + console.error('Error writing Javascript resolver to file: ' + outputJSResolverFile); } @@ -498,7 +535,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; } @@ -535,7 +576,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, @@ -548,9 +589,10 @@ async function main() { isNeptuneIAMAuth, neptuneHost, neptunePort, - outputFolderPath ); + outputFolderPath, + neptuneType ); } catch (err) { - console.error('Error creating AWS pipeline: ' + err); + console.error('Error creating AWS pipeline: ' + err); } } @@ -572,30 +614,33 @@ async function main() { inputCDKpipelineFile = `${outputFolderPath}/${inputCDKpipelineName}-cdk.js`; } - await createAWSpipelineCDK( inputCDKpipelineName, - inputCDKpipelineDatabaseName, - inputCDKpipelineRegion, - outputSchema, - schemaModel, - __dirname + outputLambdaPackagePath, - inputCDKpipelineFile, - __dirname, - quiet, - isNeptuneIAMAuth, - neptuneHost, - neptunePort, - outputFolderPath ); + 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); + 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)); diff --git a/src/pipelineResources.js b/src/pipelineResources.js index 7c75140..a66b729 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 { parseNeptuneDomainFromHost } 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 APPSYNC_API_ID = ''; +let NEPTUNE_TYPE = NEPTUNE_DB; let ZIP = null; let RESOURCES = {}; let RESOURCES_FILE = ''; @@ -99,7 +102,6 @@ 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); await lambdaClient.send(command); lambdaExists = true; } catch (error) { @@ -151,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, @@ -169,8 +173,10 @@ async function getNeptuneClusterinfoBy(name, region) { iamPolicyResource: NEPTUNE_IAM_POLICY_RESOURCE }; } - -async function getNeptuneClusterinfo() { +/** + * 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}); const params = { @@ -178,23 +184,23 @@ async function getNeptuneClusterinfo() { }; const data = await neptuneClient.send(new DescribeDBClustersCommand(params)); - + const input = { // DescribeDBSubnetGroupsMessage - DBSubnetGroupName: data.DBClusters[0].DBSubnetGroup, + 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_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); - }); + }); } @@ -214,14 +220,14 @@ 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 await sleep(10000); LAMBDA_ROLE = data.Role.Arn; - storeResource({LambdaExecutionRole: NAME +"LambdaExecutionRole"}); - if (!quiet) spinner.succeed('Role ARN: ' + yellow(LAMBDA_ROLE)); + storeResource({LambdaExecutionRole: NAME +"LambdaExecutionRole"}); + 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 +242,19 @@ async function createLambdaRole() { if (NEPTUNE_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 +263,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 }, ], @@ -308,64 +322,44 @@ async function createDeploymentPackage(folderPath) { async function createLambdaFunction() { - const lambdaClient = new LambdaClient({region: REGION}); - if (!quiet) spinner = ora('Creating Lambda function ...').start(); - - let params; - if (NEPTUNE_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" - }, - }, - }; - } 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] + 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": NEPTUNE_IAM_AUTH.toString(), + "LOGGING_ENABLED": "false", + "NEPTUNE_DB_NAME": NEPTUNE_DB_NAME, + "NEPTUNE_REGION": REGION, + "NEPTUNE_DOMAIN": parseNeptuneDomainFromHost(NEPTUNE_HOST), + "NEPTUNE_TYPE": NEPTUNE_TYPE, }, - Environment: { - Variables: { - "NEPTUNE_HOST": NEPTUNE_HOST, - "NEPTUNE_PORT": NEPTUNE_PORT, - "NEPTUNE_IAM_AUTH_ENABLED": "false", - "LOGGING_ENABLED": "false" - }, - }, - }; - } + }, + }; - const data = await lambdaClient.send(new LambdaCreateFunctionCommand(params)); - //await sleep(5000); + if (!NEPTUNE_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)); + if (!quiet) spinner.succeed('Lambda Name: ' + yellow(NAME +'LambdaFunction') + ' ARN: ' + yellow(LAMBDA_ARN)); } @@ -395,7 +389,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: [ @@ -408,7 +402,7 @@ async function createAppSyncAPI() { } ] }), - RoleName: NAME +"LambdaInvocationRole" + RoleName: NAME +"LambdaInvocationRole" }; if (!quiet) spinner = ora('Creating role for Lambda invocation ...').start(); @@ -628,7 +622,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)); } @@ -647,8 +641,8 @@ 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) { - if (!quiet) spinner.fail('AppSync API delete failed: ' + error); + } catch (error) { + if (!quiet) spinner.fail('AppSync API delete failed: ' + error); } // Lambda @@ -661,7 +655,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 +669,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 +680,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 +696,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 +710,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 +724,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 +737,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 +750,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,9 +788,22 @@ async function updateAppSyncAPI(resources) { } -async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBregion, appSyncSchema, schemaModel, lambdaFilesPath, addMutations, quietI, __dirname, isNeptuneIAMAuth, neptuneHost, neptunePort, outputFolderPath) { - - 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; @@ -809,56 +816,58 @@ 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) { 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 setNeptuneDbClusterInfo(); + 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) @@ -872,14 +881,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)); + + 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 { @@ -907,5 +916,5 @@ async function createUpdateAWSpipeline (pipelineName, neptuneDBName, neptuneDBre } } -export { createUpdateAWSpipeline, getNeptuneClusterinfoBy, removeAWSpipelineResources } +export { createUpdateAWSpipeline, getNeptuneClusterDbInfoBy, removeAWSpipelineResources } 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/src/test/util.test.js b/src/test/util.test.js new file mode 100644 index 0000000..9e10f6a --- /dev/null +++ b/src/test/util.test.js @@ -0,0 +1,37 @@ +import {parseNeptuneDomainFromEndpoint, parseNeptuneDomainFromHost, parseNeptuneGraphName} from '../util.js'; + +test('parse domain from neptune cluster host', () => { + 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(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(() => 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', () => { + 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 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 new file mode 100644 index 0000000..600a17c --- /dev/null +++ b/src/util.js @@ -0,0 +1,79 @@ +/* +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 HOST_DELIMITER = '.'; +const ENDPOINT_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(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); + } + 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 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(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]); +} + +/** + * 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 {parseNeptuneDomainFromHost, parseNeptuneDomainFromEndpoint, parseNeptuneGraphName}; \ No newline at end of file diff --git a/templates/CDKTemplate.js b/templates/CDKTemplate.js index e17c24a..c6efa34 100644 --- a/templates/CDKTemplate.js +++ b/templates/CDKTemplate.js @@ -21,6 +21,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 = '*'; @@ -34,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 { /** * @@ -54,7 +60,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: this.parseNeptuneDomainFromHost(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 +80,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 +111,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 @@ -259,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); + } } 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/templates/Lambda4AppSyncHTTP/index.mjs b/templates/Lambda4AppSyncHTTP/index.mjs index ed22a69..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: "neptune-db", + service: serviceName, }, credentials: { accessKeyId: AWS_ACCESS_KEY_ID, 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 };