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 };