├── .eslintrc.cjs ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── codeql.yml │ ├── npm-publish.yml │ └── unit.yml ├── .gitignore ├── .jest.js ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── THIRD_PARTY_LICENSES.txt ├── doc ├── cdk.md ├── cliReference.md ├── images │ ├── AppSyncQuery.jpg │ ├── todoCreate.jpg │ ├── todoGetTodos.jpg │ ├── todoNestedQuery.JPG │ └── utilityRunning.gif ├── resources.md ├── routesExample.md └── todoExample.md ├── neptune-for-graphql.mjs ├── package-lock.json ├── package.json ├── samples ├── airports.graphdb.json ├── airports.source.schema.graphql ├── changesAirport.json └── todo.schema.graphql ├── src ├── CDKPipelineApp.js ├── NeptuneSchema.js ├── changes.js ├── graphdb.js ├── help.js ├── logger.js ├── main.js ├── pipelineResources.js ├── schemaModelValidator.js ├── schemaParser.js ├── test │ ├── airports-mutations.graphql │ ├── airports-neptune-schema.json │ ├── airports.customized.graphql │ ├── airports.graphql │ ├── dining-neptune-schema.json │ ├── dining.graphql │ ├── epl-neptune-schema.json │ ├── epl.graphql │ ├── fraud-neptune-schema.json │ ├── fraud.graphql │ ├── graphdb.test.js │ ├── knowledge-neptune-schema.json │ ├── knowledge.graphql │ ├── node-edge-same-label-neptune-schema.json │ ├── node-edge-same-label.graphql │ ├── node-edge-same-property-neptune-schema.json │ ├── schemaModelValidator.test.js │ ├── security-neptune-schema.json │ ├── security.graphql │ ├── special-chars-mutations.graphql │ ├── special-chars-neptune-schema.json │ ├── special-chars.graphql │ ├── templates │ │ └── JSResolverOCHTTPS.test.js │ ├── unit.test.js.todo │ ├── user-group-validated.graphql │ ├── user-group.graphql │ ├── util-promise.test.js │ └── util.test.js ├── util-promise.js ├── util.js └── zipPackage.js ├── templates ├── ApolloServer │ ├── index.mjs │ ├── neptune.mjs │ ├── package-lock.json │ └── package.json ├── CDKTemplate.js ├── JSResolverOCHTTPS.js ├── Lambda4AppSyncGraphSDK │ ├── index.mjs │ ├── package-lock.json │ └── package.json ├── Lambda4AppSyncHTTP │ ├── index.mjs │ ├── package-lock.json │ └── package.json ├── Lambda4AppSyncSDK │ ├── index.mjs │ ├── package-lock.json │ └── package.json └── queryHttpNeptune.mjs └── test ├── TestCases ├── Case01 │ ├── Case01.01.test.js │ ├── Case01.02.test.js │ ├── Case01.03.test.js │ ├── case.json │ ├── input │ │ └── changesAirport.json │ ├── outputReference │ │ ├── output.schema.graphql │ │ └── output.source.schema.graphql │ └── queries │ │ ├── Query0000.json │ │ ├── Query0001.json │ │ ├── Query0002.json │ │ ├── Query0003.json │ │ ├── Query0004.json │ │ ├── Query0005.json │ │ ├── Query0006.json │ │ ├── Query0007.json │ │ ├── Query0008.json │ │ ├── Query0009.json │ │ ├── Query0010.json │ │ ├── Query0011.json │ │ ├── Query0012.json │ │ ├── Query0013.json │ │ ├── Query0014.json │ │ ├── Query0015.json │ │ ├── Query0016.json │ │ ├── Query0017.json │ │ ├── Query0018.json │ │ ├── Query0019.json │ │ └── Query0020.json ├── Case02 │ ├── Case02.01.test.js │ ├── case.json │ ├── input │ │ └── airports.graphdb.json │ └── queries │ │ └── Query0000.json ├── Case03 │ ├── Case03.01.test.js │ ├── case.json │ └── input │ │ └── todo.schema.graphql ├── Case04 │ ├── Case04.01.test.js │ ├── Case04.02.test.js │ ├── case.json │ ├── outputReference │ │ └── output.neptune.schema.json │ └── util.js ├── Case05 │ ├── Case05.01.test.js │ ├── Case05.02.test.js │ ├── case01.json │ └── case02.json ├── Case06 │ ├── Case06.01.test.js │ ├── Case06.02.test.js │ └── case.json ├── Case07 │ ├── Case07.01.test.js │ ├── Case07.02.test.js │ ├── Case07.03.test.js │ ├── case01.json │ ├── case02.json │ └── outputReference │ │ ├── output.schema.graphql │ │ └── output.source.schema.graphql ├── Case08 │ ├── Case08.01.test.js │ ├── Case08.02.test.js │ ├── Case08.03.test.js │ ├── Case08.04.test.js │ ├── Case08.05.test.js │ ├── Case08.06.test.js │ ├── case01.json │ ├── case02.json │ └── case03.json ├── Case09 │ ├── Case09.01.test.js │ ├── Case09.02.test.js │ ├── Case09.03.test.js │ ├── Case09.04.test.js │ ├── case01.json │ └── case02.json └── airports.source.schema.graphql ├── jestTestSequencer.js ├── package-lock.json ├── package.json └── testLib.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "overrides": [ 8 | { 9 | "env": { 10 | "node": true, 11 | "jest": true 12 | }, 13 | "files": [ 14 | ".eslintrc.{js,cjs}", 15 | "./src/test/**", 16 | "./test/**" 17 | ], 18 | "parserOptions": { 19 | "sourceType": "module" 20 | } 21 | } 22 | ], 23 | "parserOptions": { 24 | "ecmaVersion": "latest", 25 | "sourceType": "module" 26 | }, 27 | "rules": { 28 | }, 29 | "globals": { 30 | "process": true 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Issue #, if available: 2 | 3 | Description of changes: 4 | 5 | _Reminder: Add relevant entry to CHANGELOG.md_ 6 | 7 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 8 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL Advanced" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | schedule: 20 | - cron: '27 20 * * 0' 21 | 22 | jobs: 23 | analyze: 24 | name: Analyze (${{ matrix.language }}) 25 | # Runner size impacts CodeQL analysis time. To learn more, please see: 26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 27 | # - https://gh.io/supported-runners-and-hardware-resources 28 | # - https://gh.io/using-larger-runners (GitHub.com only) 29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 31 | permissions: 32 | # required for all workflows 33 | security-events: write 34 | 35 | # required to fetch internal or private CodeQL packs 36 | packages: read 37 | 38 | # only required for workflows in private repositories 39 | actions: read 40 | contents: read 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - language: javascript-typescript 47 | build-mode: none 48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 49 | # Use `c-cpp` to analyze code written in C, C++ or both 50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 56 | steps: 57 | - name: Checkout repository 58 | uses: actions/checkout@v4 59 | 60 | # Initializes the CodeQL tools for scanning. 61 | - name: Initialize CodeQL 62 | uses: github/codeql-action/init@v3 63 | with: 64 | languages: ${{ matrix.language }} 65 | build-mode: ${{ matrix.build-mode }} 66 | # If you wish to specify custom queries, you can do so here or in a config file. 67 | # By default, queries listed here will override any specified in a config file. 68 | # Prefix the list here with "+" to use these queries and those in the config file. 69 | 70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 71 | # queries: security-extended,security-and-quality 72 | 73 | # If the analyze step fails for one of the languages you are analyzing with 74 | # "We were unable to automatically build your code", modify the matrix above 75 | # to set the build mode to "manual" for that language. Then modify this step 76 | # to build your code. 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 79 | - if: matrix.build-mode == 'manual' 80 | shell: bash 81 | run: | 82 | echo 'If you are using a "manual" build mode for one or more of the' \ 83 | 'languages you are analyzing, replace this with the commands to build' \ 84 | 'your code, for example:' 85 | echo ' make bootstrap' 86 | echo ' make release' 87 | exit 1 88 | 89 | - name: Perform CodeQL Analysis 90 | uses: github/codeql-action/analyze@v3 91 | with: 92 | category: "/language:${{matrix.language}}" 93 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: workflow_dispatch 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: 20 16 | - run: npm ci 17 | 18 | publish-npm: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: 20 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm ci 28 | - run: npm publish --access public 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/unit.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | install-and-test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: 18 | - 20 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | 28 | - name: Install package and dependencies 29 | run: npm install 30 | 31 | - name: Run unit tests 32 | run: npm run test:unit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/launch.json 2 | **/node_modules/ 3 | coverage/** 4 | **/output/ 5 | *.iml 6 | *.DS_STORE 7 | -------------------------------------------------------------------------------- /.jest.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'transform': {}, 3 | 'verbose': true, 4 | 'testSequencer': './test/jestTestSequencer.js', 5 | 'globals': { 6 | // neptune db that has pre-loaded air routes sample data host and port 7 | // ex. db-neptune-foo-bar.cluster-abc.us-west-2.neptune.amazonaws.com 8 | 'AIR_ROUTES_DB_HOST': process.env.AIR_ROUTES_DB_HOST, 9 | // ex. 8182 10 | 'AIR_ROUTES_DB_PORT': process.env.AIR_ROUTES_DB_PORT 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 14 | 15 | # amazon-neptune-for-graphql CHANGELOG 16 | 17 | ## Release v1.2.0 (Release Date: TBD) 18 | 19 | This release contains new support for Apollo Server integration. 20 | 21 | ### Bug Fixes 22 | 23 | * Don't cast integers to floats in Neptune schema ([#62](https://github.com/aws/amazon-neptune-for-graphql/pull/62)) 24 | * Fix query from AppSync with an empty filter object ([#61](https://github.com/aws/amazon-neptune-for-graphql/pull/61)) 25 | * Retain numeric parameter value type when creating open cypher query ([#63](https://github.com/aws/amazon-neptune-for-graphql/pull/63)) 26 | * Fixed bug with ID argument type conversion and added Apollo arguments to help menu ([#74](https://github.com/aws/amazon-neptune-for-graphql/pull/74)) 27 | * Upgraded axios and babel versions to fix security warnings ([#90](https://github.com/aws/amazon-neptune-for-graphql/pull/90)) 28 | * Fixed failing integration test by excluding `node_modules` from Apollo zip ([#94](https://github.com/aws/amazon-neptune-for-graphql/pull/94)) 29 | * Fixed enum types in schema to be included in input types ([#95](https://github.com/aws/amazon-neptune-for-graphql/pull/95)) 30 | * Fixed bug where id fields without @id directives are not accounted for ([#96](https://github.com/aws/amazon-neptune-for-graphql/pull/96)) 31 | * Fixed custom scalar types in schema to be included in input types ([#97](https://github.com/aws/amazon-neptune-for-graphql/pull/97)) 32 | * Fixed queries generated from an input schema which retrieve an array to have an option parameter with limit ([#97](https://github.com/aws/amazon-neptune-for-graphql/pull/97)) 33 | * Fixed nested edge subqueries to return an empty array if no results were found (([#100](https://github.com/aws/amazon-neptune-for-graphql/pull/100)) 34 | * Fixed usage of variables with nested edge subqueries (([#100](https://github.com/aws/amazon-neptune-for-graphql/pull/100)) 35 | 36 | 37 | ### Features 38 | 39 | * Support output of zip package of Apollo Server artifacts (([#70](https://github.com/aws/amazon-neptune-for-graphql/pull/70)), ([#72](https://github.com/aws/amazon-neptune-for-graphql/pull/72)), ([#73](https://github.com/aws/amazon-neptune-for-graphql/pull/73)), ([#75](https://github.com/aws/amazon-neptune-for-graphql/pull/75)), ([#76](https://github.com/aws/amazon-neptune-for-graphql/pull/76))) 40 | * Allow filtering using string comparison operators `eq`, `contains`, `startsWith`, `endsWith` (([#100](https://github.com/aws/amazon-neptune-for-graphql/pull/100)) 41 | * Added pagination support through the addition of an `offset` argument in query options which can be used in combination with the existing `limit` (([#102](https://github.com/aws/amazon-neptune-for-graphql/pull/102)) 42 | 43 | 44 | ### Improvements 45 | 46 | * Increased graphdb.js test coverage using sample data ([#53](https://github.com/aws/amazon-neptune-for-graphql/pull/53)) 47 | * Saved the neptune schema to file early so that it can be used for troubleshooting ([#56](https://github.com/aws/amazon-neptune-for-graphql/pull/56)) 48 | * Alias edges with same label as a node ([#57](https://github.com/aws/amazon-neptune-for-graphql/pull/57)) 49 | * Cap concurrent requests to get Neptune schema ([#58](https://github.com/aws/amazon-neptune-for-graphql/pull/58)) 50 | * Honour @id directive on type fields ([#60](https://github.com/aws/amazon-neptune-for-graphql/pull/60)) 51 | * Changed lambda template to use ECMAScripts modules ([#68](https://github.com/aws/amazon-neptune-for-graphql/pull/68)) 52 | * Add template file missing from packaging ([#71](https://github.com/aws/amazon-neptune-for-graphql/pull/71)) 53 | * Separated graphQL schema from resolver template ([#79](https://github.com/aws/amazon-neptune-for-graphql/pull/79)) 54 | * Added unit tests for resolver and moved resolver integration tests to be unit tests ([#83](https://github.com/aws/amazon-neptune-for-graphql/pull/83)) 55 | * Set limit on the expensive query which is retrieving distinct to and from labels for edges ([#89](https://github.com/aws/amazon-neptune-for-graphql/pull/89)) 56 | * Added distinct input types for create and update mutations ([#93](https://github.com/aws/amazon-neptune-for-graphql/pull/93)) 57 | * Enabled mutations for the Apollo Server ([#98](https://github.com/aws/amazon-neptune-for-graphql/pull/98)) 58 | * Refactored integration tests to be less vulnerable to resolver logic changes ([#99](https://github.com/aws/amazon-neptune-for-graphql/pull/99)) 59 | * Enabled usage of query fragments with Apollo Server ([#103](https://github.com/aws/amazon-neptune-for-graphql/pull/103)) -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](./LICENSE.txt) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /doc/cdk.md: -------------------------------------------------------------------------------- 1 | # Deploy the GraphQL API resources with CDK 2 | If your organization require using CDK to deploy AWS resources, here an end to end example. 3 |
4 | Note: likely your organization has a CDK application that deploy other AWS resources. The team that maintain the CDK application can integrate the GraphQL API deployment using two files from the GraphQL utility output; the *output/your-new-GraphQL-API-name-cdk.js* and the *output/your-new-GraphQL-API-name.zip*. 5 | 6 | 7 | ## Create the CDK assets 8 | Example starting from an Amazon Neptune database endpoint. In this case the utility uses the Neptune endpoint to discover the region and database name. 9 | 10 | `neptune-for-graphql --input-graphdb-schema-neptune-endpoint `<*your-neptune-database-endpoint:port*>` --output-aws-pipeline-cdk --output-aws-pipeline-cdk-name` <*your-new-GraphQL-API-name*> `--output-resolver-query-https` 11 | 12 | Example starting from a GraphQL schema. 13 | 14 | `neptune-for-graphql --input-schema-file `<*your-graphql-schema-file*>` --output-aws-pipeline-cdk --output-aws-pipeline-cdk-name` <*your-new-GraphQL-API-name*>` --output-aws-pipeline-cdk-neptune-endpoint` <*your-neptune-database-endpoint:port*>` --output-resolver-query-https` 15 | 16 | 17 | 18 | ## An example on how to test the CDK utility output 19 | 20 | ### Install the CDK 21 | `npm install -g aws-cdk` 22 |
23 | `cdk --version` 24 | 25 | ### Create a CDK application 26 | Create a directory *your-CDK-dir* for your CDK application, CD in the new directory and initialize the CDK application. 27 | 28 | `md` *your-CDK-dir* 29 |
30 | `cd ` *your-CDK-dir* 31 |
32 | `cdk init app --language javascript` 33 | 34 | Install CDK libraries used by the GraphQL utility. 35 | 36 | `npm install @aws-cdk/aws-iam` 37 |
38 | `npm install @aws-cdk/aws-ec2` 39 |
40 | `npm install @aws-cdk/aws-appsync` 41 | 42 | ### Integrate the GraphQL utility CDK output to the CDK application 43 | Copy from the utility CDK output files from the *output* directory in the CDK application. 44 | 45 | `cp output/`*your-new-GraphQL-API-name*`-cdk.js /lib` 46 |
47 | `cp output/`*your-new-GraphQL-API-name*`.zip .` 48 | 49 | Update the CDK test project, editing the file `bin/`*your-CDK-dir*`.js` 50 |
51 | The original file looks like this: 52 | ```js 53 | #!/usr/bin/env node 54 | 55 | const cdk = require('aws-cdk-lib'); 56 | const { CdkStack } = require('../lib/cdk-stack'); 57 | 58 | const app = new cdk.App(); 59 | new CdkStack(app, 'CdkStack', { 60 | /* If you don't specify 'env', this stack will be environment-agnostic. 61 | * Account/Region-dependent features and context lookups will not work, 62 | * but a single synthesized template can be deployed anywhere. */ 63 | 64 | /* Uncomment the next line to specialize this stack for the AWS Account 65 | * and Region that are implied by the current CLI configuration. */ 66 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 67 | 68 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 69 | }); 70 | ``` 71 | 72 | Update the require statement to reference the `AppSyncNeptuneStack` class from the `-cdk.js` file that was copied to the `lib` directory: 73 | 74 | ```js 75 | const { AppSyncNeptuneStack } = require('../lib/your-new-GraphQL-API-name-cdk'); 76 | ``` 77 | 78 | Update the stack instantiation to reference `AppSyncNeptuneStack` and change the stack name if desired: 79 | 80 | ```js 81 | new AppSyncNeptuneStack(app, 'your-CdkStack-name', { 82 | ``` 83 | 84 | The end result should look something like this: 85 | ```js 86 | #!/usr/bin/env node 87 | 88 | const cdk = require('aws-cdk-lib'); 89 | const { AppSyncNeptuneStack } = require('../lib/your-new-GraphQL-API-name-cdk'); 90 | 91 | const app = new cdk.App(); 92 | new AppSyncNeptuneStack(app, 'your-CdkStack-name', { 93 | /* If you don't specify 'env', this stack will be environment-agnostic. 94 | * Account/Region-dependent features and context lookups will not work, 95 | * but a single synthesized template can be deployed anywhere. */ 96 | 97 | /* Uncomment the next line to specialize this stack for the AWS Account 98 | * and Region that are implied by the current CLI configuration. */ 99 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 100 | 101 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 102 | }); 103 | ``` 104 | 105 | 106 | ### Run the CDK application 107 | 108 | To create CloudFormantion template: 109 | 110 | `cdk synth` 111 | 112 | To deploy the CloudFormation template: 113 | 114 | `cdk deploy` 115 | 116 | To rollback your deployment: 117 | 118 | `cdk destroy` -------------------------------------------------------------------------------- /doc/images/AppSyncQuery.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/amazon-neptune-for-graphql/77afc73017085f8cc9b9f02ca09f35122e460cab/doc/images/AppSyncQuery.jpg -------------------------------------------------------------------------------- /doc/images/todoCreate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/amazon-neptune-for-graphql/77afc73017085f8cc9b9f02ca09f35122e460cab/doc/images/todoCreate.jpg -------------------------------------------------------------------------------- /doc/images/todoGetTodos.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/amazon-neptune-for-graphql/77afc73017085f8cc9b9f02ca09f35122e460cab/doc/images/todoGetTodos.jpg -------------------------------------------------------------------------------- /doc/images/todoNestedQuery.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/amazon-neptune-for-graphql/77afc73017085f8cc9b9f02ca09f35122e460cab/doc/images/todoNestedQuery.JPG -------------------------------------------------------------------------------- /doc/images/utilityRunning.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/amazon-neptune-for-graphql/77afc73017085f8cc9b9f02ca09f35122e460cab/doc/images/utilityRunning.gif -------------------------------------------------------------------------------- /doc/todoExample.md: -------------------------------------------------------------------------------- 1 | # TODO Example: Starting from a GraphQL schema with no directives 2 | You can start from a GraphQL schema without directives and an empty Neptune database. The utility will inference directives, input, queries and mutations, and create the the GraphQL API. Then, you can use GraphQL to create, mutate and query the data stored in a Neptune database without the need to know how to use a graph query language. 3 | 4 | In this example we start from a TODO GraphQL schema, that you can find in the [samples](https://github.com/aws/amazon-neptune-for-graphql/blob/main/samples/todo.schema.graphql). Includes two types: *Todo* and *Comment*. The *Todo* has a field *comments* as list of *Comment* type. 5 | 6 | ```graphql 7 | type Todo { 8 | name: String 9 | description: String 10 | priority: Int 11 | status: String 12 | comments: [Comment] 13 | } 14 | 15 | type Comment { 16 | content: String 17 | } 18 | ``` 19 | 20 | Let's now run this schema through the utility and create the GraphQL API in AWS AppSync. *(Note: pls provide a reachable, empty Neptune database endpoint)* 21 | 22 | `neptune-for-graphql --input-schema-file ./samples/todo.schema.graphql --create-update-aws-pipeline --create-update-aws-pipeline-name TodoExample --create-update-aws-pipeline-neptune-endpoint` <*your-neptune-database-endpoint:port*> ` --output-resolver-query-https` 23 | 24 | The utility created a new file in the *output* folder called *TodoExample.source.graphql*, and the GraphQL API in AppSync. As you can see below, the utility inferenced: 25 | 26 | - In the type *Todo* it added *@relationship* for a new type *CommentEdge*. This is instructing the resolver to connect *Todo* to *Comment* using a graph database edge called *CommentEdge*. 27 | - A new input called *TodoInput* was added to help the queries and the mutations. 28 | - For each type (*Todo*, *Comment*) the utility added two queries. One to retrive a single type using an id or any of the type fields listed in the input, and the second to retrive multiple values, filtered using the input of that type. 29 | - For each type three mutations: create, update and delete. Selecting the type to delete using an id or the input for that type. These mutation affect the data stored in The Neptune database. 30 | - For connections two mutations: connect and delete. They take as input the ids of the from and to. The ids are of used by Neptune, and the connection are edges in the graph database as mention earlier. 31 | 32 | Note: the queries and mutations you see below are recognized by the resolver based on the name pattern. If you need to customize it, first look at the documentation section: [Customize the GraphQL schema with directives](https://github.com/aws/amazon-neptune-for-graphql/blob/main/README.md/#customize-the-graphql-schema-with-directives). 33 | 34 | ```graphql 35 | type Todo { 36 | _id: ID! @id 37 | name: String 38 | description: String 39 | priority: Int 40 | status: String 41 | comments(filter: CommentInput, options: Options): [Comment] @relationship(type: "CommentEdge", direction: OUT) 42 | commentEdge: CommentEdge 43 | } 44 | 45 | type Comment { 46 | _id: ID! @id 47 | content: String 48 | } 49 | 50 | input Options { 51 | limit: Int 52 | } 53 | 54 | input TodoInput { 55 | _id: ID @id 56 | name: String 57 | description: String 58 | priority: Int 59 | status: String 60 | } 61 | 62 | type CommentEdge { 63 | _id: ID! @id 64 | } 65 | 66 | input CommentInput { 67 | _id: ID @id 68 | content: String 69 | } 70 | 71 | input Options { 72 | limit: Int 73 | } 74 | 75 | type Query { 76 | getNodeTodo(filter: TodoInput): Todo 77 | getNodeTodos(filter: TodoInput, options: Options): [Todo] 78 | getNodeComment(filter: CommentInput): Comment 79 | getNodeComments(filter: CommentInput, options: Options): [Comment] 80 | } 81 | 82 | type Mutation { 83 | createNodeTodo(input: TodoInput!): Todo 84 | updateNodeTodo(input: TodoInput!): Todo 85 | deleteNodeTodo(_id: ID!): Boolean 86 | connectNodeTodoToNodeCommentEdgeCommentEdge(from_id: ID!, to_id: ID!): CommentEdge 87 | deleteEdgeCommentEdgeFromTodoToComment(from_id: ID!, to_id: ID!): Boolean 88 | createNodeComment(input: CommentInput!): Comment 89 | updateNodeComment(input: CommentInput!): Comment 90 | deleteNodeComment(_id: ID!): Boolean 91 | } 92 | 93 | schema { 94 | query: Query 95 | mutation: Mutation 96 | } 97 | ``` 98 | 99 | Now we are ready to create and query our data. 100 | 101 | Here below snapshot of the AppSync Queries console used to test our new GraphQL API named *TodoExampleAPI*. 102 | In the middle window, the Explorer shows you the queries and mutations. You can then pick a query, the input parameters and the return fields. 103 | 104 | The picture shows the creation of a node type *Todo*, using *createNodeTodo* mutation. 105 | 106 | ![Create](https://github.com/aws/amazon-neptune-for-graphql/blob/main/doc/images/todoCreate.jpg) 107 | 108 | Here quering all the Todos with *getNodeTodos* query. 109 | 110 | ![Query](https://github.com/aws/amazon-neptune-for-graphql/blob/main/doc/images/todoGetTodos.jpg) 111 | 112 | After having created one *Comment* using *createNodeComment*, we used the Ids connect them using the mutation *connectNodeTodoToNodeCommentEdgeCommentEdge* 113 | 114 | Here a nested query to retreive Todos and their attached comments. 115 | 116 | ![Query](https://github.com/aws/amazon-neptune-for-graphql/blob/main/doc/images/todoNestedQuery.JPG) 117 | 118 | The solution illustrated in this example if functional and can be used as is. If you wish you can make changes to the *TodoExample.source.graphql* following the direction in the section: *Customize the GraphQL schema with directives*. The edited schema can then be use as `--input-schema-file` running the utility again. The utility will then modify the GraphQL API. 119 | -------------------------------------------------------------------------------- /neptune-for-graphql.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | Licensed under the Apache License, Version 2.0 (the "License"). 5 | You may not use this file except in compliance with the License. 6 | A copy of the License is located at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | or in the "license" file accompanying this file. This file is distributed 9 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 10 | express or implied. See the License for the specific language governing 11 | permissions and limitations under the License. 12 | */ 13 | 14 | import { main } from './src/main.js'; 15 | 16 | // If no arguments are provided, suggest help 17 | if (process.argv.length <= 2) { 18 | console.log("--h for help.\n"); 19 | process.exit(0); 20 | } 21 | 22 | main(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@aws/neptune-for-graphql", 3 | "version": "1.2.0", 4 | "description": "CLI utility to create and maintain a GraphQL API for Amazon Neptune", 5 | "keywords": [ 6 | "Amazon Neptune", 7 | "Neptune", 8 | "graph database", 9 | "GraphQL", 10 | "AppSync", 11 | "Amplify" 12 | ], 13 | "homepage": "https://github.com/aws/amazon-neptune-for-graphql#readme", 14 | "main": "neptune-for-graphql", 15 | "bugs": { 16 | "url": "https://github.com/aws/amazon-neptune-for-graphql/issues" 17 | }, 18 | "directories": { 19 | "test": "test" 20 | }, 21 | "scripts": { 22 | "postinstall": "cd templates/Lambda4AppSyncHTTP && npm install && cd ../Lambda4AppSyncSDK && npm install && cd ../Lambda4AppSyncGraphSDK && npm install && cd ../ApolloServer && npm install", 23 | "lint": "eslint neptune-for-graphql.mjs ./src", 24 | "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js", 25 | "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=src/test", 26 | "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=test/TestCases", 27 | "test:sdk": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=test/TestCases/Case07", 28 | "test:resolver": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --runInBand --detectOpenHandles --config .jest.js --testPathPattern=test/TestCases/Case01" 29 | }, 30 | "jest": { 31 | "collectCoverage": true, 32 | "coverageReporters": [ 33 | "json", 34 | "html" 35 | ] 36 | }, 37 | "bin": { 38 | "neptune-for-graphql": "./neptune-for-graphql.mjs" 39 | }, 40 | "files": [ 41 | "neptune-for-graphql.mjs", 42 | "./templates/CDKTemplate.js", 43 | "./templates/queryHttpNeptune.mjs", 44 | "./templates/JSResolverOCHTTPS.js", 45 | "./templates/Lambda4AppSyncHTTP/index.mjs", 46 | "./templates/Lambda4AppSyncHTTP/package.json", 47 | "./templates/Lambda4AppSyncSDK/index.mjs", 48 | "./templates/Lambda4AppSyncSDK/package.json", 49 | "./templates/Lambda4AppSyncGraphSDK/index.mjs", 50 | "./templates/Lambda4AppSyncGraphSDK/package.json", 51 | "./templates/ApolloServer/index.mjs", 52 | "./templates/ApolloServer/neptune.mjs", 53 | "./templates/ApolloServer/package.json", 54 | "./src/**" 55 | ], 56 | "author": "AWS", 57 | "license": "Apache-2.0", 58 | "type": "module", 59 | "dependencies": { 60 | "@aws-sdk/client-appsync": "3.387.0", 61 | "@aws-sdk/client-iam": "3.387.0", 62 | "@aws-sdk/client-lambda": "3.387.0", 63 | "@aws-sdk/client-neptune": "3.387.0", 64 | "@aws-sdk/client-neptune-graph": "3.662.0", 65 | "@aws-sdk/client-neptunedata": "3.403.0", 66 | "@aws-sdk/credential-providers": "3.414.0", 67 | "archiver": "7.0.1", 68 | "aws4-axios": "3.3.0", 69 | "axios": "^1.9.0", 70 | "graphql": "^16.8.1", 71 | "graphql-tag": "2.12.6", 72 | "ora": "7.0.1", 73 | "pino": "9.4.0", 74 | "pino-pretty": "11.2.2", 75 | "prettier": "^3.5.3", 76 | "semver": "7.5.4" 77 | }, 78 | "devDependencies": { 79 | "@jest/test-sequencer": "^29.7.0", 80 | "eslint": "^8.50.0", 81 | "jest": "^29.7.0" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /samples/airports.graphdb.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "continent", 5 | "properties": [ 6 | { "name": "code", "type": "String" }, 7 | { "name": "type", "type": "String" }, 8 | { "name": "desc", "type": "String" } 9 | ] 10 | }, 11 | { 12 | "label": "country", 13 | "properties": [ 14 | { "name": "code", "type": "String" }, 15 | { "name": "type", "type": "String" }, 16 | { "name": "desc", "type": "String" } 17 | ] 18 | }, 19 | { 20 | "label": "version", 21 | "properties": [ 22 | { "name": "date", "type": "String" }, 23 | { "name": "code", "type": "String" }, 24 | { "name": "author", "type": "String" }, 25 | { "name": "type", "type": "String" }, 26 | { "name": "desc", "type": "String" } 27 | ] 28 | }, 29 | { 30 | "label": "airport", 31 | "properties": [ 32 | { "name": "country", "type": "String" }, 33 | { "name": "longest", "type": "Int" }, 34 | { "name": "code", "type": "String" }, 35 | { "name": "city", "type": "String" }, 36 | { "name": "elev", "type": "Int" }, 37 | { "name": "icao", "type": "String" }, 38 | { "name": "lon", "type": "Float" }, 39 | { "name": "runways", "type": "Int" }, 40 | { "name": "region", "type": "String" }, 41 | { "name": "type", "type": "String" }, 42 | { "name": "lat", "type": "Float" }, 43 | { "name": "desc", "type": "String" } 44 | ] 45 | } 46 | ], 47 | "edgeStructures": [ 48 | { 49 | "label": "contains", 50 | "properties": [], 51 | "directions": [ 52 | { "from": "continent", "to": "airport", "relationship": "ONE-MANY" }, 53 | { "from": "country", "to": "airport", "relationship": "ONE-MANY" } 54 | ] 55 | }, 56 | { 57 | "label": "route", 58 | "properties": [ 59 | { "name": "dist", "type": "Int" } 60 | ], 61 | "directions": [ 62 | { "from": "airport", "to": "airport", "relationship": "MANY-MANY" } 63 | ] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /samples/airports.source.schema.graphql: -------------------------------------------------------------------------------- 1 | type Continent @alias(property: "continent") { 2 | id: ID! @id 3 | code: String 4 | type: String 5 | desc: String 6 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) 7 | contains: Contains 8 | } 9 | 10 | input ContinentInput { 11 | id: ID @id 12 | code: String 13 | type: String 14 | desc: String 15 | } 16 | 17 | type Country @alias(property: "country") { 18 | _id: ID! @id 19 | code: String 20 | type: String 21 | desc: String 22 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) 23 | contains: Contains 24 | } 25 | 26 | input CountryInput { 27 | _id: ID @id 28 | code: String 29 | type: String 30 | desc: String 31 | } 32 | 33 | type Version @alias(property: "version") { 34 | _id: ID! @id 35 | date: String 36 | code: String 37 | author: String 38 | type: String 39 | desc: String 40 | } 41 | 42 | input VersionInput { 43 | _id: ID @id 44 | date: String 45 | code: String 46 | author: String 47 | type: String 48 | desc: String 49 | } 50 | 51 | type Airport @alias(property: "airport") { 52 | _id: ID! @id 53 | country: String 54 | longest: Int 55 | code: String 56 | city: String 57 | elev: Int 58 | icao: String 59 | lon: Float 60 | runways: Int 61 | region: String 62 | type: String 63 | lat: Float 64 | desc2: String @alias(property: "desc") 65 | outboundRoutesCount: Int @graphQuery(statement: "MATCH (this)-[r:route]->(a) RETURN count(r)") 66 | continentContainsIn: Continent @relationship(edgeType: "contains", direction: IN) 67 | countryContainsIn: Country @relationship(edgeType: "contains", direction: IN) 68 | airportRoutesOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: OUT) 69 | airportRoutesIn(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: IN) 70 | contains: Contains 71 | route: Route 72 | } 73 | 74 | input AirportInput { 75 | _id: ID @id 76 | country: String 77 | longest: Int 78 | code: String 79 | city: String 80 | elev: Int 81 | icao: String 82 | lon: Float 83 | runways: Int 84 | region: String 85 | type: String 86 | lat: Float 87 | desc: String 88 | } 89 | 90 | type Contains @alias(property: "contains") { 91 | _id: ID! @id 92 | } 93 | 94 | type Route @alias(property: "route") { 95 | _id: ID! @id 96 | dist: Int 97 | } 98 | 99 | input RouteInput { 100 | dist: Int 101 | } 102 | 103 | input Options { 104 | limit: Int 105 | } 106 | 107 | type Query { 108 | getAirport(code: String): Airport 109 | getAirportConnection(fromCode: String!, toCode: String!): Airport @cypher(statement: "MATCH (:airport{code: '$fromCode'})-[:route]->(this:airport)-[:route]->(:airport{code:'$toCode'})") 110 | getAirportWithGremlin(code:String): Airport @graphQuery(statement: "g.V().has('airport', 'code', '$code').elementMap()") 111 | getAirportsWithGremlin: [Airport] @graphQuery(statement: "g.V().hasLabel('airport').elementMap().fold()") 112 | getCountriesCountGremlin: Int @graphQuery(statement: "g.V().hasLabel('country').count()") 113 | 114 | getNodeContinent(filter: ContinentInput): Continent 115 | getNodeContinents(filter: ContinentInput, options: Options): [Continent] 116 | getNodeCountry(filter: CountryInput): Country 117 | getNodeCountrys(filter: CountryInput, options: Options): [Country] 118 | getNodeVersion(filter: VersionInput): Version 119 | getNodeVersions(filter: VersionInput, options: Options): [Version] 120 | getNodeAirport(filter: AirportInput): Airport 121 | getNodeAirports(filter: AirportInput, options: Options): [Airport] 122 | } 123 | 124 | type Mutation { 125 | createAirport(input: AirportInput!): Airport @graphQuery(statement: "CREATE (this:airport {$input}) RETURN this") 126 | 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") 127 | deleteAirport(id: ID): Int @graphQuery(statement: "MATCH (this:airport) WHERE ID(this) = '$id' DETACH DELETE this") 128 | 129 | createNodeContinent(input: ContinentInput!): Continent 130 | updateNodeContinent(input: ContinentInput!): Continent 131 | deleteNodeContinent(_id: ID!): Boolean 132 | createNodeCountry(input: CountryInput!): Country 133 | updateNodeCountry(input: CountryInput!): Country 134 | deleteNodeCountry(_id: ID!): Boolean 135 | createNodeVersion(input: VersionInput!): Version 136 | updateNodeVersion(input: VersionInput!): Version 137 | deleteNodeVersion(_id: ID!): Boolean 138 | createNodeAirport(input: AirportInput!): Airport 139 | updateNodeAirport(input: AirportInput!): Airport 140 | deleteNodeAirport(_id: ID!): Boolean 141 | connectNodeContinentToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 142 | deleteEdgeContainsFromContinentToAirport(from_id: ID!, to_id: ID!): Boolean 143 | connectNodeCountryToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 144 | deleteEdgeContainsFromCountryToAirport(from_id: ID!, to_id: ID!): Boolean 145 | connectNodeAirportToNodeAirportEdgeRoute(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 146 | updateEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 147 | deleteEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!): Boolean 148 | } 149 | 150 | schema { 151 | query: Query 152 | mutation: Mutation 153 | } -------------------------------------------------------------------------------- /samples/changesAirport.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "type": "Airport", "field": "outboundRoutesCountAdd", "action": "add", "value":"outboundRoutesCountAdd: Int @graphQuery(statement: \"MATCH (this)-[r:route]->(a) RETURN count(r)\")"}, 3 | { "type": "Mutation", "field": "deleteNodeVersion", "action": "remove", "value": "" }, 4 | { "type": "Mutation", "field": "createNodeVersion", "action": "remove", "value": "" } 5 | ] -------------------------------------------------------------------------------- /samples/todo.schema.graphql: -------------------------------------------------------------------------------- 1 | 2 | type Todo { 3 | name: String 4 | description: String 5 | priority: Int 6 | status: String 7 | comments: [Comment] 8 | bestComment: Comment 9 | } 10 | 11 | type Comment { 12 | content: String 13 | } -------------------------------------------------------------------------------- /src/changes.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Licensed under the Apache License, Version 2.0 (the "License"). 4 | You may not use this file except in compliance with the License. 5 | A copy of the License is located at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | or in the "license" file accompanying this file. This file is distributed 8 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 9 | express or implied. See the License for the specific language governing 10 | permissions and limitations under the License. 11 | */ 12 | 13 | function addChanges(changesDirectives, currentType) { 14 | /* Alternative 15 | return changesDirectives 16 | .filter(change => change.type === currentType && change.action == "acc") 17 | .map(change => change.value) 18 | .join("\n") 19 | + "\n" 20 | */ 21 | let r = ''; 22 | changesDirectives.forEach(change => { 23 | if (change.type == currentType && change.action == 'add') { 24 | r += change.value + '\n'; 25 | } 26 | }); 27 | return r; 28 | } 29 | 30 | 31 | function removeChanges(changesDirectives, currentType, line) { 32 | let r = line; 33 | 34 | changesDirectives.forEach(change => { 35 | if (change.type == currentType && change.action == 'remove' && line.startsWith(change.field)) { 36 | r = '*** REMOVE ***'; 37 | } 38 | }); 39 | 40 | return r; 41 | } 42 | 43 | 44 | function changeGraphQLSchema(schema, changes) { 45 | const changesDirectives = JSON.parse(changes); 46 | 47 | 48 | let lines = schema.split('\n'); 49 | let r = ''; 50 | 51 | let currentType = ''; 52 | for (const linel of lines) { 53 | let line = linel.trim(); 54 | let parts = line.split(' '); 55 | 56 | if (line.startsWith('type ')) { 57 | currentType = parts[1]; 58 | } 59 | 60 | if (line.startsWith('}')) { 61 | r += addChanges(changesDirectives, currentType); 62 | currentType = ''; 63 | } 64 | 65 | line = removeChanges(changesDirectives, currentType, line); 66 | 67 | if (line != '*** REMOVE ***') { 68 | r += line + '\n'; 69 | } 70 | } 71 | 72 | return r; 73 | } 74 | 75 | 76 | export { changeGraphQLSchema} -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | import { pino } from "pino"; 2 | import pretty from "pino-pretty"; 3 | import path from "path"; 4 | 5 | let fileLogger; 6 | let logFileDestination; 7 | 8 | /** 9 | * Initialize the standard out and file loggers. 10 | * @param directory the directory in which to create the log file 11 | * @param quiet true if the standard output should be minimalized to errors only 12 | * @param logLevel the file log level 13 | */ 14 | function loggerInit(directory, quiet = false, logLevel = 'info') { 15 | // replaces characters that windows does not allow in filenames 16 | logFileDestination = path.join(directory, 'log_' + new Date().toISOString().replaceAll(/[.:]/g, '-') + '.txt'); 17 | const streams = [ 18 | { 19 | level: logLevel, 20 | stream: pretty({ 21 | destination: logFileDestination, 22 | mkdir: true, 23 | colorize: false, 24 | translateTime: 'yyyy-mm-dd HH:MM:ss', 25 | ignore: 'pid,hostname' 26 | }) 27 | }, 28 | ] 29 | 30 | // using pino.multistream seems to resolve some issues with file logging in windows environments that occurred when pino.transport was used instead 31 | fileLogger = pino({ 32 | level: logLevel 33 | }, pino.multistream(streams)); 34 | if (quiet) { 35 | console.log = function(){}; 36 | console.info = function(){}; 37 | console.debug = function(){}; 38 | } 39 | } 40 | 41 | function log(level, text, options = {toConsole: false}) { 42 | let detail = options.detail; 43 | if (detail) { 44 | if (options.toConsole) { 45 | console.log(text + ': ' + yellow(detail)); 46 | } 47 | fileLogger[level](removeYellow(text) + ': ' + removeYellow(detail)); 48 | } else { 49 | if (options.toConsole) { 50 | console.log(text); 51 | } 52 | // remove any yellow which may have been added by the caller 53 | fileLogger[level](removeYellow(text)); 54 | } 55 | } 56 | 57 | function loggerInfo(text, options = {toConsole: false}) { 58 | log('info', text, options); 59 | } 60 | 61 | function loggerDebug(text, options = {toConsole: false}) { 62 | log('debug', text, options); 63 | } 64 | 65 | /** 66 | * Log an error to console and file. A simplified error message will be output to console while a more detailed error will be logged to file. 67 | * @param errorMessage the error message to log to console and file 68 | * @param error optional error object that caused the error 69 | */ 70 | function loggerError(errorMessage, error) { 71 | let toConsole = errorMessage; 72 | let toLog = removeYellow(errorMessage); 73 | if (error) { 74 | toConsole = toConsole + ': ' + error.message + ' - Please see ' + logFileDestination + ' for more details'; 75 | toLog = toLog + '\n' + JSON.stringify(error, null, 4); 76 | } 77 | console.error(toConsole); 78 | fileLogger.error(toLog); 79 | } 80 | 81 | function yellow(text) { 82 | return '\x1b[33m' + text + '\x1b[0m'; 83 | } 84 | 85 | function removeYellow(text) { 86 | let withoutYellow = text.replaceAll(/\x1b\[33m/g, ''); 87 | return withoutYellow.replaceAll(/\x1b\[0m/g, ''); 88 | } 89 | 90 | export { loggerInit, loggerInfo, loggerError, loggerDebug, yellow }; -------------------------------------------------------------------------------- /src/schemaParser.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Licensed under the Apache License, Version 2.0 (the "License"). 4 | You may not use this file except in compliance with the License. 5 | A copy of the License is located at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | or in the "license" file accompanying this file. This file is distributed 8 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 9 | express or implied. See the License for the specific language governing 10 | permissions and limitations under the License. 11 | */ 12 | 13 | import gql from 'graphql-tag'; 14 | import { print, visit } from 'graphql'; 15 | 16 | 17 | function schemaParser (schema) { 18 | const typeDefs = gql` 19 | ${schema} 20 | `; 21 | return typeDefs; 22 | } 23 | 24 | function schemaStringify (schemaModel, directives = true) { 25 | let r = ''; 26 | if (directives) { 27 | r = print(schemaModel); 28 | } else { 29 | const schemaWithoutDirectives = visit(schemaModel, { 30 | Directive: () => null, 31 | }); 32 | const schemaTxt = print(schemaWithoutDirectives); 33 | r = changeComments(schemaTxt); 34 | r = addSchemaType(r); 35 | } 36 | return r; 37 | } 38 | 39 | 40 | function changeComments(schemaTxt) { 41 | const lines = schemaTxt.split('\n'); 42 | const modifiedLines = lines.map(line => { 43 | line = line.replace(/^(\s*)"""/, '$1#'); 44 | line = line.replace(/"""(\s*)$/, '$1'); 45 | return line; 46 | }); 47 | 48 | const r = modifiedLines.join('\n'); 49 | return r; 50 | } 51 | 52 | 53 | function addSchemaType(schemaTxt) { 54 | if (!schemaTxt.includes('schema {')) { 55 | schemaTxt += '\n\nschema {\n query: Query\n mutation: Mutation\n}'; 56 | } 57 | return schemaTxt; 58 | } 59 | 60 | 61 | export { schemaParser, schemaStringify }; 62 | -------------------------------------------------------------------------------- /src/test/airports-neptune-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "continent", 5 | "properties": [ 6 | { 7 | "name": "type", 8 | "type": "String" 9 | }, 10 | { 11 | "name": "code", 12 | "type": "String" 13 | }, 14 | { 15 | "name": "desc", 16 | "type": "String" 17 | } 18 | ] 19 | }, 20 | { 21 | "label": "country", 22 | "properties": [ 23 | { 24 | "name": "type", 25 | "type": "String" 26 | }, 27 | { 28 | "name": "code", 29 | "type": "String" 30 | }, 31 | { 32 | "name": "desc", 33 | "type": "String" 34 | } 35 | ] 36 | }, 37 | { 38 | "label": "version", 39 | "properties": [ 40 | { 41 | "name": "date", 42 | "type": "String" 43 | }, 44 | { 45 | "name": "desc", 46 | "type": "String" 47 | }, 48 | { 49 | "name": "author", 50 | "type": "String" 51 | }, 52 | { 53 | "name": "type", 54 | "type": "String" 55 | }, 56 | { 57 | "name": "code", 58 | "type": "String" 59 | } 60 | ] 61 | }, 62 | { 63 | "label": "airport", 64 | "properties": [ 65 | { 66 | "name": "type", 67 | "type": "String" 68 | }, 69 | { 70 | "name": "city", 71 | "type": "String" 72 | }, 73 | { 74 | "name": "icao", 75 | "type": "String" 76 | }, 77 | { 78 | "name": "code", 79 | "type": "String" 80 | }, 81 | { 82 | "name": "country", 83 | "type": "String" 84 | }, 85 | { 86 | "name": "lat", 87 | "type": "Float" 88 | }, 89 | { 90 | "name": "longest", 91 | "type": "Int" 92 | }, 93 | { 94 | "name": "runways", 95 | "type": "Int" 96 | }, 97 | { 98 | "name": "desc", 99 | "type": "String" 100 | }, 101 | { 102 | "name": "lon", 103 | "type": "Float" 104 | }, 105 | { 106 | "name": "region", 107 | "type": "String" 108 | }, 109 | { 110 | "name": "elev", 111 | "type": "Int" 112 | } 113 | ] 114 | } 115 | ], 116 | "edgeStructures": [ 117 | { 118 | "label": "contains", 119 | "properties": [], 120 | "directions": [ 121 | { 122 | "from": "continent", 123 | "to": "airport", 124 | "relationship": "ONE-MANY" 125 | }, 126 | { 127 | "from": "country", 128 | "to": "airport", 129 | "relationship": "ONE-MANY" 130 | } 131 | ] 132 | }, 133 | { 134 | "label": "route", 135 | "properties": [ 136 | { 137 | "name": "dist", 138 | "type": "Int" 139 | } 140 | ], 141 | "directions": [ 142 | { 143 | "from": "airport", 144 | "to": "airport", 145 | "relationship": "MANY-MANY" 146 | } 147 | ] 148 | } 149 | ] 150 | } 151 | -------------------------------------------------------------------------------- /src/test/airports.customized.graphql: -------------------------------------------------------------------------------- 1 | type Continent @alias(property:"continent") { 2 | _id: ID! @id 3 | type: String 4 | code: String 5 | desc: String 6 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType:"contains", direction:OUT) 7 | contains:Contains 8 | } 9 | 10 | input ContinentInput { 11 | _id: ID @id 12 | type: StringScalarFilters 13 | code: StringScalarFilters 14 | desc: StringScalarFilters 15 | } 16 | 17 | input ContinentCreateInput { 18 | _id: ID @id 19 | type: String 20 | code: String 21 | desc: String 22 | } 23 | 24 | input ContinentUpdateInput { 25 | _id: ID! @id 26 | type: String 27 | code: String 28 | desc: String 29 | } 30 | 31 | type Country @alias(property:"country") { 32 | _id: ID! @id 33 | type: String 34 | code: String 35 | desc: String 36 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType:"contains", direction:OUT) 37 | contains:Contains 38 | } 39 | 40 | input CountryInput { 41 | _id: ID @id 42 | type: StringScalarFilters 43 | code: StringScalarFilters 44 | desc: StringScalarFilters 45 | } 46 | 47 | input CountryCreateInput { 48 | _id: ID @id 49 | type: String 50 | code: String 51 | desc: String 52 | } 53 | 54 | input CountryUpdateInput { 55 | _id: ID! @id 56 | type: String 57 | code: String 58 | desc: String 59 | } 60 | 61 | type Version @alias(property:"version") { 62 | _id: ID! @id 63 | date: String 64 | desc: String 65 | author: String 66 | type: String 67 | code: String 68 | } 69 | 70 | input VersionInput { 71 | _id: ID @id 72 | date: StringScalarFilters 73 | desc: StringScalarFilters 74 | author: StringScalarFilters 75 | type: StringScalarFilters 76 | code: StringScalarFilters 77 | } 78 | 79 | input VersionCreateInput { 80 | _id: ID @id 81 | date: String 82 | desc: String 83 | author: String 84 | type: String 85 | code: String 86 | } 87 | 88 | input VersionUpdateInput { 89 | _id: ID! @id 90 | date: String 91 | desc: String 92 | author: String 93 | type: String 94 | code: String 95 | } 96 | 97 | type Airport @alias(property:"airport") { 98 | _id: ID! @id 99 | type: String 100 | city: String 101 | icao: String 102 | code: String 103 | country: String 104 | lat: Float 105 | longest: Int 106 | runways: Int 107 | desc: String 108 | lon: Float 109 | region: String 110 | elev: Int 111 | outboundRoutesCount: Int @graphQuery(statement: "MATCH (this)-[r:route]->(a) RETURN count(r)") 112 | continentContainsIn: Continent @relationship(edgeType:"contains", direction:IN) 113 | countryContainsIn: Country @relationship(edgeType:"contains", direction:IN) 114 | airportRoutesOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType:"route", direction:OUT) 115 | airportRoutesIn(filter: AirportInput, options: Options): [Airport] @relationship(edgeType:"route", direction:IN) 116 | contains:Contains 117 | route:Route 118 | } 119 | 120 | input AirportInput { 121 | _id: ID @id 122 | type: StringScalarFilters 123 | city: StringScalarFilters 124 | icao: StringScalarFilters 125 | code: StringScalarFilters 126 | country: StringScalarFilters 127 | lat: Float 128 | longest: Int 129 | runways: Int 130 | desc: StringScalarFilters 131 | lon: Float 132 | region: StringScalarFilters 133 | elev: Int 134 | } 135 | 136 | input AirportCreateInput { 137 | _id: ID @id 138 | type: String 139 | city: String 140 | icao: String 141 | code: String 142 | country: String 143 | lat: Float 144 | longest: Int 145 | runways: Int 146 | desc: String 147 | lon: Float 148 | region: String 149 | elev: Int 150 | } 151 | 152 | input AirportUpdateInput { 153 | _id: ID! @id 154 | type: String 155 | city: String 156 | icao: String 157 | code: String 158 | country: String 159 | lat: Float 160 | longest: Int 161 | runways: Int 162 | desc: String 163 | lon: Float 164 | region: String 165 | elev: Int 166 | } 167 | 168 | type Contains @alias(property:"contains") { 169 | _id: ID! @id 170 | } 171 | 172 | type Route @alias(property:"route") { 173 | _id: ID! @id 174 | dist: Int 175 | } 176 | 177 | input RouteInput { 178 | dist: Int 179 | } 180 | 181 | input Options { 182 | limit:Int 183 | offset:Int 184 | } 185 | 186 | input StringScalarFilters { 187 | eq: String 188 | contains: String 189 | endsWith: String 190 | startsWith: String 191 | } 192 | 193 | type Query { 194 | getAirport(code: String): Airport 195 | getAirportConnection(fromCode: String!, toCode: String!): Airport @cypher(statement: "MATCH (:airport{code: '$fromCode'})-[:route]->(this:airport)-[:route]->(:airport{code:'$toCode'})") 196 | getContinentsWithGremlin: [Continent] @graphQuery(statement: "g.V().hasLabel('continent').elementMap().fold()") 197 | getCountriesCountGremlin: Int @graphQuery(statement: "g.V().hasLabel('country').count()") 198 | getNodeContinent(filter: ContinentInput): Continent 199 | getNodeContinents(filter: ContinentInput, options: Options): [Continent] 200 | getNodeCountry(filter: CountryInput): Country 201 | getNodeCountrys(filter: CountryInput, options: Options): [Country] 202 | getNodeVersion(filter: VersionInput): Version 203 | getNodeVersions(filter: VersionInput, options: Options): [Version] 204 | getNodeAirport(filter: AirportInput): Airport 205 | getNodeAirports(filter: AirportInput, options: Options): [Airport] 206 | getAirportWithGremlin(code: String): Airport @graphQuery(statement: "g.V().has('airport', 'code', '$code').elementMap()") 207 | getCountriesCount: Int @graphQuery(statement: "g.V().hasLabel('country').count()") 208 | } 209 | 210 | type Mutation { 211 | createNodeAirport(input: AirportCreateInput!): Airport 212 | updateNodeAirport(input: AirportUpdateInput!): Airport 213 | connectNodeCountryToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 214 | deleteEdgeContainsFromCountryToAirport(from_id: ID!, to_id: ID!): Boolean 215 | updateEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 216 | createAirport(input: AirportCreateInput!): Airport @graphQuery(statement: "CREATE (this:airport {$input}) RETURN this") 217 | } 218 | 219 | schema { 220 | query: Query 221 | mutation: Mutation 222 | } -------------------------------------------------------------------------------- /src/test/airports.graphql: -------------------------------------------------------------------------------- 1 | type Continent @alias(property: "continent") { 2 | _id: ID! @id 3 | type: String 4 | code: String 5 | desc: String 6 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) 7 | contains: Contains 8 | } 9 | 10 | input ContinentInput { 11 | _id: ID @id 12 | type: StringScalarFilters 13 | code: StringScalarFilters 14 | desc: StringScalarFilters 15 | } 16 | 17 | type Country @alias(property: "country") { 18 | _id: ID! @id 19 | type: String 20 | code: String 21 | desc: String 22 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) 23 | contains: Contains 24 | } 25 | 26 | input CountryInput { 27 | _id: ID @id 28 | type: StringScalarFilters 29 | code: StringScalarFilters 30 | desc: StringScalarFilters 31 | } 32 | 33 | type Version @alias(property: "version") { 34 | _id: ID! @id 35 | date: String 36 | desc: String 37 | author: String 38 | type: String 39 | code: String 40 | } 41 | 42 | input VersionInput { 43 | _id: ID @id 44 | date: StringScalarFilters 45 | desc: StringScalarFilters 46 | author: StringScalarFilters 47 | type: StringScalarFilters 48 | code: StringScalarFilters 49 | } 50 | 51 | type Airport @alias(property: "airport") { 52 | _id: ID! @id 53 | type: String 54 | city: String 55 | icao: String 56 | code: String 57 | country: String 58 | lat: Float 59 | longest: Int 60 | runways: Int 61 | desc: String 62 | lon: Float 63 | region: String 64 | elev: Int 65 | continentContainsIn: Continent @relationship(edgeType: "contains", direction: IN) 66 | countryContainsIn: Country @relationship(edgeType: "contains", direction: IN) 67 | airportRoutesOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: OUT) 68 | airportRoutesIn(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: IN) 69 | contains: Contains 70 | route: Route 71 | } 72 | 73 | input AirportInput { 74 | _id: ID @id 75 | type: StringScalarFilters 76 | city: StringScalarFilters 77 | icao: StringScalarFilters 78 | code: StringScalarFilters 79 | country: StringScalarFilters 80 | lat: Float 81 | longest: Int 82 | runways: Int 83 | desc: StringScalarFilters 84 | lon: Float 85 | region: StringScalarFilters 86 | elev: Int 87 | } 88 | 89 | type Contains @alias(property: "contains") { 90 | _id: ID! @id 91 | } 92 | 93 | type Route @alias(property: "route") { 94 | _id: ID! @id 95 | dist: Int 96 | } 97 | 98 | input RouteInput { 99 | dist: Int 100 | } 101 | 102 | input Options { 103 | limit: Int 104 | offset: Int 105 | } 106 | 107 | input StringScalarFilters { 108 | eq: String 109 | contains: String 110 | endsWith: String 111 | startsWith: String 112 | } 113 | 114 | type Query { 115 | getNodeContinent(filter: ContinentInput): Continent 116 | getNodeContinents(filter: ContinentInput, options: Options): [Continent] 117 | getNodeCountry(filter: CountryInput): Country 118 | getNodeCountrys(filter: CountryInput, options: Options): [Country] 119 | getNodeVersion(filter: VersionInput): Version 120 | getNodeVersions(filter: VersionInput, options: Options): [Version] 121 | getNodeAirport(filter: AirportInput): Airport 122 | getNodeAirports(filter: AirportInput, options: Options): [Airport] 123 | } 124 | 125 | schema { 126 | query: Query 127 | } 128 | -------------------------------------------------------------------------------- /src/test/dining-neptune-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "city", 5 | "properties": [ 6 | { 7 | "name": "name", 8 | "type": "String" 9 | } 10 | ] 11 | }, 12 | { 13 | "label": "review", 14 | "properties": [ 15 | { 16 | "name": "body", 17 | "type": "String" 18 | }, 19 | { 20 | "name": "created_date", 21 | "type": "String" 22 | }, 23 | { 24 | "name": "rating", 25 | "type": "Int" 26 | } 27 | ] 28 | }, 29 | { 30 | "label": "person", 31 | "properties": [ 32 | { 33 | "name": "first_name", 34 | "type": "String" 35 | }, 36 | { 37 | "name": "last_name", 38 | "type": "String" 39 | }, 40 | { 41 | "name": "person_id", 42 | "type": "Int" 43 | } 44 | ] 45 | }, 46 | { 47 | "label": "restaurant", 48 | "properties": [ 49 | { 50 | "name": "restaurant_id", 51 | "type": "Int" 52 | }, 53 | { 54 | "name": "name", 55 | "type": "String" 56 | }, 57 | { 58 | "name": "address", 59 | "type": "String" 60 | } 61 | ] 62 | }, 63 | { 64 | "label": "cuisine", 65 | "properties": [ 66 | { 67 | "name": "name", 68 | "type": "String" 69 | } 70 | ] 71 | }, 72 | { 73 | "label": "state", 74 | "properties": [ 75 | { 76 | "name": "name", 77 | "type": "String" 78 | } 79 | ] 80 | } 81 | ], 82 | "edgeStructures": [ 83 | { 84 | "label": "lives", 85 | "properties": [], 86 | "directions": [ 87 | { 88 | "from": "person", 89 | "to": "city", 90 | "relationship": "MANY-ONE" 91 | } 92 | ] 93 | }, 94 | { 95 | "label": "within", 96 | "properties": [], 97 | "directions": [ 98 | { 99 | "from": "city", 100 | "to": "state", 101 | "relationship": "ONE-ONE" 102 | }, 103 | { 104 | "from": "restaurant", 105 | "to": "city", 106 | "relationship": "MANY-ONE" 107 | } 108 | ] 109 | }, 110 | { 111 | "label": "serves", 112 | "properties": [], 113 | "directions": [ 114 | { 115 | "from": "restaurant", 116 | "to": "cuisine", 117 | "relationship": "MANY-ONE" 118 | } 119 | ] 120 | }, 121 | { 122 | "label": "wrote", 123 | "properties": [], 124 | "directions": [ 125 | { 126 | "from": "person", 127 | "to": "review", 128 | "relationship": "ONE-MANY" 129 | } 130 | ] 131 | }, 132 | { 133 | "label": "about", 134 | "properties": [], 135 | "directions": [ 136 | { 137 | "from": "review", 138 | "to": "restaurant", 139 | "relationship": "MANY-ONE" 140 | } 141 | ] 142 | }, 143 | { 144 | "label": "friends", 145 | "properties": [], 146 | "directions": [ 147 | { 148 | "from": "person", 149 | "to": "person", 150 | "relationship": "MANY-MANY" 151 | } 152 | ] 153 | } 154 | ] 155 | } 156 | -------------------------------------------------------------------------------- /src/test/dining.graphql: -------------------------------------------------------------------------------- 1 | type City @alias(property: "city") { 2 | _id: ID! @id 3 | name: String 4 | personLivessIn(filter: PersonInput, options: Options): [Person] @relationship(edgeType: "lives", direction: IN) 5 | restaurantWithinsIn(filter: RestaurantInput, options: Options): [Restaurant] @relationship(edgeType: "within", direction: IN) 6 | lives: Lives 7 | within: Within 8 | } 9 | 10 | input CityInput { 11 | _id: ID @id 12 | name: StringScalarFilters 13 | } 14 | 15 | type Review @alias(property: "review") { 16 | _id: ID! @id 17 | body: String 18 | created_date: String 19 | rating: Int 20 | personWroteIn: Person @relationship(edgeType: "wrote", direction: IN) 21 | restaurantAboutOut: Restaurant @relationship(edgeType: "about", direction: OUT) 22 | wrote: Wrote 23 | about: About 24 | } 25 | 26 | input ReviewInput { 27 | _id: ID @id 28 | body: StringScalarFilters 29 | created_date: StringScalarFilters 30 | rating: Int 31 | } 32 | 33 | type Person @alias(property: "person") { 34 | _id: ID! @id 35 | first_name: String 36 | last_name: String 37 | person_id: Int 38 | cityLivesOut: City @relationship(edgeType: "lives", direction: OUT) 39 | reviewWrotesOut(filter: ReviewInput, options: Options): [Review] @relationship(edgeType: "wrote", direction: OUT) 40 | personFriendssOut(filter: PersonInput, options: Options): [Person] @relationship(edgeType: "friends", direction: OUT) 41 | personFriendssIn(filter: PersonInput, options: Options): [Person] @relationship(edgeType: "friends", direction: IN) 42 | lives: Lives 43 | wrote: Wrote 44 | friends: Friends 45 | } 46 | 47 | input PersonInput { 48 | _id: ID @id 49 | first_name: StringScalarFilters 50 | last_name: StringScalarFilters 51 | person_id: Int 52 | } 53 | 54 | type Restaurant @alias(property: "restaurant") { 55 | _id: ID! @id 56 | restaurant_id: Int 57 | name: String 58 | address: String 59 | cityWithinOut: City @relationship(edgeType: "within", direction: OUT) 60 | cuisineServesOut: Cuisine @relationship(edgeType: "serves", direction: OUT) 61 | reviewAboutsIn(filter: ReviewInput, options: Options): [Review] @relationship(edgeType: "about", direction: IN) 62 | within: Within 63 | serves: Serves 64 | about: About 65 | } 66 | 67 | input RestaurantInput { 68 | _id: ID @id 69 | restaurant_id: Int 70 | name: StringScalarFilters 71 | address: StringScalarFilters 72 | } 73 | 74 | type Cuisine @alias(property: "cuisine") { 75 | _id: ID! @id 76 | name: String 77 | restaurantServessIn(filter: RestaurantInput, options: Options): [Restaurant] @relationship(edgeType: "serves", direction: IN) 78 | serves: Serves 79 | } 80 | 81 | input CuisineInput { 82 | _id: ID @id 83 | name: StringScalarFilters 84 | } 85 | 86 | type State @alias(property: "state") { 87 | _id: ID! @id 88 | name: String 89 | within: Within 90 | } 91 | 92 | input StateInput { 93 | _id: ID @id 94 | name: StringScalarFilters 95 | } 96 | 97 | type Lives @alias(property: "lives") { 98 | _id: ID! @id 99 | } 100 | 101 | type Within @alias(property: "within") { 102 | _id: ID! @id 103 | } 104 | 105 | type Serves @alias(property: "serves") { 106 | _id: ID! @id 107 | } 108 | 109 | type Wrote @alias(property: "wrote") { 110 | _id: ID! @id 111 | } 112 | 113 | type About @alias(property: "about") { 114 | _id: ID! @id 115 | } 116 | 117 | type Friends @alias(property: "friends") { 118 | _id: ID! @id 119 | } 120 | 121 | input Options { 122 | limit: Int 123 | offset: Int 124 | } 125 | 126 | input StringScalarFilters { 127 | eq: String 128 | contains: String 129 | endsWith: String 130 | startsWith: String 131 | } 132 | 133 | type Query { 134 | getNodeCity(filter: CityInput): City 135 | getNodeCitys(filter: CityInput, options: Options): [City] 136 | getNodeReview(filter: ReviewInput): Review 137 | getNodeReviews(filter: ReviewInput, options: Options): [Review] 138 | getNodePerson(filter: PersonInput): Person 139 | getNodePersons(filter: PersonInput, options: Options): [Person] 140 | getNodeRestaurant(filter: RestaurantInput): Restaurant 141 | getNodeRestaurants(filter: RestaurantInput, options: Options): [Restaurant] 142 | getNodeCuisine(filter: CuisineInput): Cuisine 143 | getNodeCuisines(filter: CuisineInput, options: Options): [Cuisine] 144 | getNodeState(filter: StateInput): State 145 | getNodeStates(filter: StateInput, options: Options): [State] 146 | } 147 | 148 | schema { 149 | query: Query 150 | } 151 | -------------------------------------------------------------------------------- /src/test/epl-neptune-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "Stadium", 5 | "properties": [ 6 | { 7 | "name": "opened", 8 | "type": "Int" 9 | }, 10 | { 11 | "name": "capacity", 12 | "type": "Int" 13 | }, 14 | { 15 | "name": "name", 16 | "type": "String" 17 | } 18 | ] 19 | }, 20 | { 21 | "label": "League", 22 | "properties": [ 23 | { 24 | "name": "nickname", 25 | "type": "String" 26 | }, 27 | { 28 | "name": "name", 29 | "type": "String" 30 | } 31 | ] 32 | }, 33 | { 34 | "label": "Team", 35 | "properties": [ 36 | { 37 | "name": "nickname", 38 | "type": "String" 39 | }, 40 | { 41 | "name": "name", 42 | "type": "String" 43 | }, 44 | { 45 | "name": "fullName", 46 | "type": "String" 47 | }, 48 | { 49 | "name": "founded", 50 | "type": "Int" 51 | } 52 | ] 53 | }, 54 | { 55 | "label": "City", 56 | "properties": [ 57 | { 58 | "name": "name", 59 | "type": "String" 60 | } 61 | ] 62 | } 63 | ], 64 | "edgeStructures": [ 65 | { 66 | "label": "CITY_EDGE", 67 | "properties": [], 68 | "directions": [ 69 | { 70 | "from": "Stadium", 71 | "to": "City", 72 | "relationship": "MANY-ONE" 73 | } 74 | ] 75 | }, 76 | { 77 | "label": "CURRENT_LEAGUE", 78 | "properties": [], 79 | "directions": [ 80 | { 81 | "from": "Team", 82 | "to": "League", 83 | "relationship": "MANY-ONE" 84 | } 85 | ] 86 | }, 87 | { 88 | "label": "STADIUM_EDGE", 89 | "properties": [], 90 | "directions": [ 91 | { 92 | "from": "Team", 93 | "to": "Stadium", 94 | "relationship": "ONE-ONE" 95 | } 96 | ] 97 | } 98 | ] 99 | } 100 | -------------------------------------------------------------------------------- /src/test/epl.graphql: -------------------------------------------------------------------------------- 1 | type Stadium { 2 | _id: ID! @id 3 | opened: Int 4 | capacity: Int 5 | name: String 6 | cityCity_edgeOut: City @relationship(edgeType: "CITY_EDGE", direction: OUT) 7 | CITY_EDGE: City_edge 8 | STADIUM_EDGE: Stadium_edge 9 | } 10 | 11 | input StadiumInput { 12 | _id: ID @id 13 | opened: Int 14 | capacity: Int 15 | name: StringScalarFilters 16 | } 17 | 18 | type League { 19 | _id: ID! @id 20 | nickname: String 21 | name: String 22 | teamCurrent_leaguesIn(filter: TeamInput, options: Options): [Team] @relationship(edgeType: "CURRENT_LEAGUE", direction: IN) 23 | CURRENT_LEAGUE: Current_league 24 | } 25 | 26 | input LeagueInput { 27 | _id: ID @id 28 | nickname: StringScalarFilters 29 | name: StringScalarFilters 30 | } 31 | 32 | type Team { 33 | _id: ID! @id 34 | nickname: String 35 | name: String 36 | fullName: String 37 | founded: Int 38 | leagueCurrent_leagueOut: League @relationship(edgeType: "CURRENT_LEAGUE", direction: OUT) 39 | CURRENT_LEAGUE: Current_league 40 | STADIUM_EDGE: Stadium_edge 41 | } 42 | 43 | input TeamInput { 44 | _id: ID @id 45 | nickname: StringScalarFilters 46 | name: StringScalarFilters 47 | fullName: StringScalarFilters 48 | founded: Int 49 | } 50 | 51 | type City { 52 | _id: ID! @id 53 | name: String 54 | stadiumCity_edgesIn(filter: StadiumInput, options: Options): [Stadium] @relationship(edgeType: "CITY_EDGE", direction: IN) 55 | CITY_EDGE: City_edge 56 | } 57 | 58 | input CityInput { 59 | _id: ID @id 60 | name: StringScalarFilters 61 | } 62 | 63 | type City_edge @alias(property: "CITY_EDGE") { 64 | _id: ID! @id 65 | } 66 | 67 | type Current_league @alias(property: "CURRENT_LEAGUE") { 68 | _id: ID! @id 69 | } 70 | 71 | type Stadium_edge @alias(property: "STADIUM_EDGE") { 72 | _id: ID! @id 73 | } 74 | 75 | input Options { 76 | limit: Int 77 | offset: Int 78 | } 79 | 80 | input StringScalarFilters { 81 | eq: String 82 | contains: String 83 | endsWith: String 84 | startsWith: String 85 | } 86 | 87 | type Query { 88 | getNodeStadium(filter: StadiumInput): Stadium 89 | getNodeStadiums(filter: StadiumInput, options: Options): [Stadium] 90 | getNodeLeague(filter: LeagueInput): League 91 | getNodeLeagues(filter: LeagueInput, options: Options): [League] 92 | getNodeTeam(filter: TeamInput): Team 93 | getNodeTeams(filter: TeamInput, options: Options): [Team] 94 | getNodeCity(filter: CityInput): City 95 | getNodeCitys(filter: CityInput, options: Options): [City] 96 | } 97 | 98 | schema { 99 | query: Query 100 | } 101 | -------------------------------------------------------------------------------- /src/test/fraud-neptune-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "DateOfBirth", 5 | "properties": [ 6 | { 7 | "name": "value", 8 | "type": "String" 9 | } 10 | ] 11 | }, 12 | { 13 | "label": "Account", 14 | "properties": [ 15 | { 16 | "name": "first_name", 17 | "type": "String" 18 | }, 19 | { 20 | "name": "account_number", 21 | "type": "String" 22 | }, 23 | { 24 | "name": "last_name", 25 | "type": "String" 26 | } 27 | ] 28 | }, 29 | { 30 | "label": "Merchant", 31 | "properties": [ 32 | { 33 | "name": "name", 34 | "type": "String" 35 | } 36 | ] 37 | }, 38 | { 39 | "label": "Transaction", 40 | "properties": [ 41 | { 42 | "name": "amount", 43 | "type": "Int" 44 | }, 45 | { 46 | "name": "created", 47 | "type": "String" 48 | } 49 | ] 50 | }, 51 | { 52 | "label": "Address", 53 | "properties": [ 54 | { 55 | "name": "value", 56 | "type": "String" 57 | } 58 | ] 59 | }, 60 | { 61 | "label": "PhoneNumber", 62 | "properties": [ 63 | { 64 | "name": "value", 65 | "type": "String" 66 | } 67 | ] 68 | }, 69 | { 70 | "label": "IpAddress", 71 | "properties": [ 72 | { 73 | "name": "value", 74 | "type": "String" 75 | } 76 | ] 77 | }, 78 | { 79 | "label": "EmailAddress", 80 | "properties": [ 81 | { 82 | "name": "value", 83 | "type": "String" 84 | } 85 | ] 86 | } 87 | ], 88 | "edgeStructures": [ 89 | { 90 | "label": "FEATURE_OF_ACCOUNT", 91 | "properties": [], 92 | "directions": [ 93 | { 94 | "from": "EmailAddress", 95 | "to": "Account", 96 | "relationship": "MANY-MANY" 97 | }, 98 | { 99 | "from": "IpAddress", 100 | "to": "Account", 101 | "relationship": "ONE-MANY" 102 | }, 103 | { 104 | "from": "Address", 105 | "to": "Account", 106 | "relationship": "ONE-MANY" 107 | }, 108 | { 109 | "from": "DateOfBirth", 110 | "to": "Account", 111 | "relationship": "ONE-MANY" 112 | }, 113 | { 114 | "from": "PhoneNumber", 115 | "to": "Account", 116 | "relationship": "MANY-MANY" 117 | } 118 | ] 119 | }, 120 | { 121 | "label": "ACCOUNT_EDGE", 122 | "properties": [], 123 | "directions": [ 124 | { 125 | "from": "Transaction", 126 | "to": "Account", 127 | "relationship": "MANY-ONE" 128 | } 129 | ] 130 | }, 131 | { 132 | "label": "FEATURE_OF_TRANSACTION", 133 | "properties": [], 134 | "directions": [ 135 | { 136 | "from": "IpAddress", 137 | "to": "Transaction", 138 | "relationship": "ONE-MANY" 139 | }, 140 | { 141 | "from": "PhoneNumber", 142 | "to": "Transaction", 143 | "relationship": "ONE-MANY" 144 | } 145 | ] 146 | }, 147 | { 148 | "label": "MERCHANT_EDGE", 149 | "properties": [], 150 | "directions": [ 151 | { 152 | "from": "Transaction", 153 | "to": "Merchant", 154 | "relationship": "MANY-ONE" 155 | } 156 | ] 157 | } 158 | ] 159 | } 160 | -------------------------------------------------------------------------------- /src/test/graphdb.test.js: -------------------------------------------------------------------------------- 1 | import { graphDBInferenceSchema } from '../graphdb.js'; 2 | import fs from "fs"; 3 | import { loggerInit } from "../logger.js"; 4 | import * as prettier from "prettier"; 5 | 6 | test('node with same property and edge label should add underscore prefix', () => { 7 | expect(graphDBInferenceSchema(readFile('./src/test/node-edge-same-property-neptune-schema.json'), false)).toContain('_commonName:Commonname'); 8 | }); 9 | 10 | test('should properly replace special chars in schema', async () => { 11 | const actual = await inferGraphQLSchema('./src/test/special-chars-neptune-schema.json'); 12 | const expected = await loadGraphQLSchema('./src/test/special-chars.graphql'); 13 | expect(actual).toBe(expected); 14 | }); 15 | 16 | test('should correctly generate mutation input types after replacing special characters in schema', async () => { 17 | const actual = await inferGraphQLSchema('./src/test/special-chars-neptune-schema.json', { addMutations: true }); 18 | const expected = await loadGraphQLSchema('./src/test/special-chars-mutations.graphql'); 19 | expect(actual).toBe(expected); 20 | }); 21 | 22 | test('should output airport schema', async () => { 23 | const actual = await inferGraphQLSchema('./src/test/airports-neptune-schema.json'); 24 | const expected = await loadGraphQLSchema('./src/test/airports.graphql'); 25 | expect(actual).toBe(expected); 26 | }); 27 | 28 | test('should correctly generate mutation input types after outputting airport schema', async () => { 29 | const actual = await inferGraphQLSchema('./src/test/airports-neptune-schema.json', { addMutations: true }); 30 | const expected = await loadGraphQLSchema('./src/test/airports-mutations.graphql'); 31 | expect(actual).toBe(expected); 32 | }); 33 | 34 | test('should output dining by friends schema', async () => { 35 | const actual = await inferGraphQLSchema('./src/test/dining-neptune-schema.json'); 36 | const expected = await loadGraphQLSchema('./src/test/dining.graphql'); 37 | expect(actual).toBe(expected); 38 | }); 39 | 40 | test('should output epl schema', async () => { 41 | const actual = await inferGraphQLSchema('./src/test/epl-neptune-schema.json'); 42 | const expected = await loadGraphQLSchema('./src/test/epl.graphql'); 43 | expect(actual).toBe(expected); 44 | }); 45 | 46 | test('should output fraud graph schema', async () => { 47 | const actual = await inferGraphQLSchema('./src/test/fraud-neptune-schema.json'); 48 | const expected = await loadGraphQLSchema('./src/test/fraud.graphql'); 49 | expect(actual).toBe(expected); 50 | }); 51 | 52 | test('should output knowledge graph schema', async () => { 53 | const actual = await inferGraphQLSchema('./src/test/knowledge-neptune-schema.json'); 54 | const expected = await loadGraphQLSchema('./src/test/knowledge.graphql'); 55 | expect(actual).toBe(expected); 56 | }); 57 | 58 | test('should output security graph schema', async () => { 59 | const actual = await inferGraphQLSchema('./src/test/security-neptune-schema.json'); 60 | const expected = await loadGraphQLSchema('./src/test/security.graphql'); 61 | expect(actual).toBe(expected); 62 | }); 63 | 64 | test('should alias edge with same label as node', async () => { 65 | loggerInit('./src/test/output', false, 'info'); 66 | const actual = await inferGraphQLSchema('./src/test/node-edge-same-label-neptune-schema.json'); 67 | const expected = await loadGraphQLSchema('./src/test/node-edge-same-label.graphql'); 68 | expect(actual).toBe(expected); 69 | }); 70 | 71 | async function inferGraphQLSchema(neptuneSchemaFilePath, options = { addMutations: false }) { 72 | let neptuneSchema = readFile(neptuneSchemaFilePath); 73 | let inferredSchema = graphDBInferenceSchema(neptuneSchema, options.addMutations); 74 | return await sanitizeWhitespace(inferredSchema); 75 | } 76 | 77 | async function loadGraphQLSchema(graphQLSchemaFilePath) { 78 | let expectedSchema = readFile(graphQLSchemaFilePath); 79 | return await sanitizeWhitespace(expectedSchema); 80 | } 81 | 82 | async function sanitizeWhitespace(str) { 83 | // max printWidth to prevent line wrapping (which tends to wrap in unexpected ways) 84 | return await prettier.format(str, {parser: "graphql", printWidth: Number.MAX_VALUE}); 85 | } 86 | 87 | function readFile(fileName) { 88 | return fs.readFileSync(fileName, 'utf8'); 89 | } 90 | -------------------------------------------------------------------------------- /src/test/knowledge-neptune-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "date", 5 | "properties": [ 6 | { 7 | "name": "type", 8 | "type": "String" 9 | }, 10 | { 11 | "name": "text", 12 | "type": "String" 13 | } 14 | ] 15 | }, 16 | { 17 | "label": "other", 18 | "properties": [ 19 | { 20 | "name": "type", 21 | "type": "String" 22 | }, 23 | { 24 | "name": "text", 25 | "type": "String" 26 | } 27 | ] 28 | }, 29 | { 30 | "label": "post", 31 | "properties": [ 32 | { 33 | "name": "title", 34 | "type": "String" 35 | }, 36 | { 37 | "name": "post_date", 38 | "type": "String" 39 | } 40 | ] 41 | }, 42 | { 43 | "label": "author", 44 | "properties": [ 45 | { 46 | "name": "name", 47 | "type": "String" 48 | } 49 | ] 50 | }, 51 | { 52 | "label": "organization", 53 | "properties": [ 54 | { 55 | "name": "type", 56 | "type": "String" 57 | }, 58 | { 59 | "name": "text", 60 | "type": "String" 61 | } 62 | ] 63 | }, 64 | { 65 | "label": "location", 66 | "properties": [ 67 | { 68 | "name": "type", 69 | "type": "String" 70 | }, 71 | { 72 | "name": "text", 73 | "type": "String" 74 | } 75 | ] 76 | }, 77 | { 78 | "label": "tag", 79 | "properties": [ 80 | { 81 | "name": "tag", 82 | "type": "String" 83 | } 84 | ] 85 | }, 86 | { 87 | "label": "title", 88 | "properties": [ 89 | { 90 | "name": "type", 91 | "type": "String" 92 | }, 93 | { 94 | "name": "text", 95 | "type": "String" 96 | } 97 | ] 98 | }, 99 | { 100 | "label": "commercial_item", 101 | "properties": [ 102 | { 103 | "name": "type", 104 | "type": "String" 105 | }, 106 | { 107 | "name": "text", 108 | "type": "String" 109 | } 110 | ] 111 | } 112 | ], 113 | "edgeStructures": [ 114 | { 115 | "label": "tagged", 116 | "properties": [], 117 | "directions": [ 118 | { 119 | "from": "post", 120 | "to": "tag", 121 | "relationship": "MANY-MANY" 122 | } 123 | ] 124 | }, 125 | { 126 | "label": "found_in", 127 | "properties": [ 128 | { 129 | "name": "score", 130 | "type": "Float" 131 | } 132 | ], 133 | "directions": [ 134 | { 135 | "from": "post", 136 | "to": "organization", 137 | "relationship": "MANY-MANY" 138 | }, 139 | { 140 | "from": "post", 141 | "to": "title", 142 | "relationship": "MANY-MANY" 143 | }, 144 | { 145 | "from": "post", 146 | "to": "location", 147 | "relationship": "MANY-MANY" 148 | }, 149 | { 150 | "from": "post", 151 | "to": "date", 152 | "relationship": "MANY-MANY" 153 | }, 154 | { 155 | "from": "post", 156 | "to": "commercial_item", 157 | "relationship": "MANY-MANY" 158 | }, 159 | { 160 | "from": "post", 161 | "to": "other", 162 | "relationship": "ONE-MANY" 163 | } 164 | ] 165 | }, 166 | { 167 | "label": "written_by", 168 | "properties": [], 169 | "directions": [ 170 | { 171 | "from": "post", 172 | "to": "author", 173 | "relationship": "MANY-MANY" 174 | } 175 | ] 176 | } 177 | ] 178 | } 179 | -------------------------------------------------------------------------------- /src/test/knowledge.graphql: -------------------------------------------------------------------------------- 1 | type Date @alias(property: "date") { 2 | _id: ID! @id 3 | type: String 4 | text: String 5 | postFound_insIn(filter: PostInput, options: Options): [Post] @relationship(edgeType: "found_in", direction: IN) 6 | found_in: Found_in 7 | } 8 | 9 | input DateInput { 10 | _id: ID @id 11 | type: StringScalarFilters 12 | text: StringScalarFilters 13 | } 14 | 15 | type Other @alias(property: "other") { 16 | _id: ID! @id 17 | type: String 18 | text: String 19 | postFound_inIn: Post @relationship(edgeType: "found_in", direction: IN) 20 | found_in: Found_in 21 | } 22 | 23 | input OtherInput { 24 | _id: ID @id 25 | type: StringScalarFilters 26 | text: StringScalarFilters 27 | } 28 | 29 | type Post @alias(property: "post") { 30 | _id: ID! @id 31 | title: String 32 | post_date: String 33 | tagTaggedsOut(filter: TagInput, options: Options): [Tag] @relationship(edgeType: "tagged", direction: OUT) 34 | organizationFound_insOut(filter: OrganizationInput, options: Options): [Organization] @relationship(edgeType: "found_in", direction: OUT) 35 | titleFound_insOut(filter: TitleInput, options: Options): [Title] @relationship(edgeType: "found_in", direction: OUT) 36 | locationFound_insOut(filter: LocationInput, options: Options): [Location] @relationship(edgeType: "found_in", direction: OUT) 37 | dateFound_insOut(filter: DateInput, options: Options): [Date] @relationship(edgeType: "found_in", direction: OUT) 38 | commercial_itemFound_insOut(filter: Commercial_itemInput, options: Options): [Commercial_item] @relationship(edgeType: "found_in", direction: OUT) 39 | otherFound_insOut(filter: OtherInput, options: Options): [Other] @relationship(edgeType: "found_in", direction: OUT) 40 | authorWritten_bysOut(filter: AuthorInput, options: Options): [Author] @relationship(edgeType: "written_by", direction: OUT) 41 | tagged: Tagged 42 | found_in: Found_in 43 | written_by: Written_by 44 | } 45 | 46 | input PostInput { 47 | _id: ID @id 48 | title: StringScalarFilters 49 | post_date: StringScalarFilters 50 | } 51 | 52 | type Author @alias(property: "author") { 53 | _id: ID! @id 54 | name: String 55 | postWritten_bysIn(filter: PostInput, options: Options): [Post] @relationship(edgeType: "written_by", direction: IN) 56 | written_by: Written_by 57 | } 58 | 59 | input AuthorInput { 60 | _id: ID @id 61 | name: StringScalarFilters 62 | } 63 | 64 | type Organization @alias(property: "organization") { 65 | _id: ID! @id 66 | type: String 67 | text: String 68 | postFound_insIn(filter: PostInput, options: Options): [Post] @relationship(edgeType: "found_in", direction: IN) 69 | found_in: Found_in 70 | } 71 | 72 | input OrganizationInput { 73 | _id: ID @id 74 | type: StringScalarFilters 75 | text: StringScalarFilters 76 | } 77 | 78 | type Location @alias(property: "location") { 79 | _id: ID! @id 80 | type: String 81 | text: String 82 | postFound_insIn(filter: PostInput, options: Options): [Post] @relationship(edgeType: "found_in", direction: IN) 83 | found_in: Found_in 84 | } 85 | 86 | input LocationInput { 87 | _id: ID @id 88 | type: StringScalarFilters 89 | text: StringScalarFilters 90 | } 91 | 92 | type Tag @alias(property: "tag") { 93 | _id: ID! @id 94 | tag: String 95 | postTaggedsIn(filter: PostInput, options: Options): [Post] @relationship(edgeType: "tagged", direction: IN) 96 | tagged: Tagged 97 | } 98 | 99 | input TagInput { 100 | _id: ID @id 101 | tag: StringScalarFilters 102 | } 103 | 104 | type Title @alias(property: "title") { 105 | _id: ID! @id 106 | type: String 107 | text: String 108 | postFound_insIn(filter: PostInput, options: Options): [Post] @relationship(edgeType: "found_in", direction: IN) 109 | found_in: Found_in 110 | } 111 | 112 | input TitleInput { 113 | _id: ID @id 114 | type: StringScalarFilters 115 | text: StringScalarFilters 116 | } 117 | 118 | type Commercial_item @alias(property: "commercial_item") { 119 | _id: ID! @id 120 | type: String 121 | text: String 122 | postFound_insIn(filter: PostInput, options: Options): [Post] @relationship(edgeType: "found_in", direction: IN) 123 | found_in: Found_in 124 | } 125 | 126 | input Commercial_itemInput { 127 | _id: ID @id 128 | type: StringScalarFilters 129 | text: StringScalarFilters 130 | } 131 | 132 | type Tagged @alias(property: "tagged") { 133 | _id: ID! @id 134 | } 135 | 136 | type Found_in @alias(property: "found_in") { 137 | _id: ID! @id 138 | score: Float 139 | } 140 | 141 | input Found_inInput { 142 | score: Float 143 | } 144 | 145 | type Written_by @alias(property: "written_by") { 146 | _id: ID! @id 147 | } 148 | 149 | input Options { 150 | limit: Int 151 | offset: Int 152 | } 153 | 154 | input StringScalarFilters { 155 | eq: String 156 | contains: String 157 | endsWith: String 158 | startsWith: String 159 | } 160 | 161 | type Query { 162 | getNodeDate(filter: DateInput): Date 163 | getNodeDates(filter: DateInput, options: Options): [Date] 164 | getNodeOther(filter: OtherInput): Other 165 | getNodeOthers(filter: OtherInput, options: Options): [Other] 166 | getNodePost(filter: PostInput): Post 167 | getNodePosts(filter: PostInput, options: Options): [Post] 168 | getNodeAuthor(filter: AuthorInput): Author 169 | getNodeAuthors(filter: AuthorInput, options: Options): [Author] 170 | getNodeOrganization(filter: OrganizationInput): Organization 171 | getNodeOrganizations(filter: OrganizationInput, options: Options): [Organization] 172 | getNodeLocation(filter: LocationInput): Location 173 | getNodeLocations(filter: LocationInput, options: Options): [Location] 174 | getNodeTag(filter: TagInput): Tag 175 | getNodeTags(filter: TagInput, options: Options): [Tag] 176 | getNodeTitle(filter: TitleInput): Title 177 | getNodeTitles(filter: TitleInput, options: Options): [Title] 178 | getNodeCommercial_item(filter: Commercial_itemInput): Commercial_item 179 | getNodeCommercial_items(filter: Commercial_itemInput, options: Options): [Commercial_item] 180 | } 181 | 182 | schema { 183 | query: Query 184 | } 185 | -------------------------------------------------------------------------------- /src/test/node-edge-same-label-neptune-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "post", 5 | "properties": [ 6 | { 7 | "name": "text", 8 | "type": "String" 9 | } 10 | ] 11 | }, 12 | { 13 | "label": "author", 14 | "properties": [ 15 | { 16 | "name": "username", 17 | "type": "String" 18 | } 19 | ] 20 | } 21 | ], 22 | "edgeStructures": [ 23 | { 24 | "label": "author", 25 | "properties": [ 26 | { 27 | "name": "date", 28 | "type": "Date" 29 | } 30 | ], 31 | "directions": [ 32 | { 33 | "from": "post", 34 | "to": "author", 35 | "relationship": "ONE-ONE" 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/test/node-edge-same-label.graphql: -------------------------------------------------------------------------------- 1 | type post { 2 | _id: ID! @id 3 | text: String 4 | author: author 5 | } 6 | 7 | input postInput { 8 | _id: ID @id 9 | text: StringScalarFilters 10 | } 11 | 12 | type author { 13 | _id: ID! @id 14 | username: String 15 | author: author 16 | } 17 | 18 | input authorInput { 19 | _id: ID @id 20 | username: StringScalarFilters 21 | } 22 | 23 | type _author @alias(property: "author") { 24 | _id: ID! @id 25 | date: Date 26 | } 27 | 28 | input _authorInput { 29 | date: Date 30 | } 31 | 32 | input Options { 33 | limit: Int 34 | offset: Int 35 | } 36 | 37 | input StringScalarFilters { 38 | eq: String 39 | contains: String 40 | endsWith: String 41 | startsWith: String 42 | } 43 | 44 | type Query { 45 | getNodepost(filter: postInput): post 46 | getNodeposts(filter: postInput, options: Options): [post] 47 | getNodeauthor(filter: authorInput): author 48 | getNodeauthors(filter: authorInput, options: Options): [author] 49 | } 50 | 51 | schema { 52 | query: Query 53 | } 54 | -------------------------------------------------------------------------------- /src/test/node-edge-same-property-neptune-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "continent", 5 | "properties": [ 6 | { 7 | "name": "id", 8 | "type": "String" 9 | }, 10 | { 11 | "name": "code", 12 | "type": "String" 13 | }, 14 | { 15 | "name": "desc", 16 | "type": "String" 17 | }, 18 | { 19 | "name": "commonName", 20 | "type": "String" 21 | } 22 | ] 23 | }, 24 | { 25 | "label": "country", 26 | "properties": [ 27 | { 28 | "name": "id", 29 | "type": "String" 30 | }, 31 | { 32 | "name": "code", 33 | "type": "String" 34 | }, 35 | { 36 | "name": "desc", 37 | "type": "String" 38 | } 39 | ] 40 | } 41 | ], 42 | "edgeStructures": [ 43 | { 44 | "label": "contains", 45 | "properties": [ 46 | { 47 | "name": "id", 48 | "type": "String" 49 | } 50 | ], 51 | "directions": [ 52 | { 53 | "from": "continent", 54 | "to": "country", 55 | "relationship": "ONE-MANY" 56 | } 57 | ] 58 | }, 59 | { 60 | "label": "commonName", 61 | "properties": [ 62 | { 63 | "name": "id", 64 | "type": "String" 65 | } 66 | ], 67 | "directions": [ 68 | { 69 | "from": "continent", 70 | "to": "country", 71 | "relationship": "ONE-MANY" 72 | } 73 | ] 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src/test/schemaModelValidator.test.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import { loggerInit } from '../logger.js'; 3 | import { validatedSchemaModel } from '../schemaModelValidator.js'; 4 | import { schemaParser, schemaStringify } from '../schemaParser.js'; 5 | 6 | describe('validatedSchemaModel', () => { 7 | let model; 8 | 9 | beforeAll(() => { 10 | loggerInit('./output', false, 'silent'); 11 | 12 | const schema = readFileSync('./src/test/user-group.graphql'); 13 | model = validatedSchemaModel(schemaParser(schema)); 14 | }); 15 | 16 | test('types definitions should be as expected', () => { 17 | const objectTypes = model.definitions.filter(def => def.kind === 'ObjectTypeDefinition').map(def => def.name.value); 18 | expect(objectTypes).toEqual(expect.arrayContaining(['User','Group','Moderator','Query', 'Mutation'])); 19 | const inputTypes = model.definitions.filter(def => def.kind === 'InputObjectTypeDefinition').map(def => def.name.value); 20 | expect(inputTypes).toEqual(expect.arrayContaining(['UserInput','GroupInput','ModeratorInput','Options'])); 21 | const enumTypes = model.definitions.filter(def => def.kind === 'EnumTypeDefinition').map(def => def.name.value); 22 | expect(enumTypes).toEqual(expect.arrayContaining(['Role'])); 23 | }); 24 | 25 | test('should only add _id field to object types without ID fields', () => { 26 | const objTypeDefs = model.definitions.filter(def => def.kind === 'ObjectTypeDefinition'); 27 | const userType = objTypeDefs.find(def => def.name.value === 'User'); 28 | const groupType = objTypeDefs.find(def => def.name.value === 'Group'); 29 | const moderatorType = objTypeDefs.find(def => def.name.value === 'Moderator'); 30 | 31 | expect(userType.fields).toHaveLength(5); 32 | expect(groupType.fields).toHaveLength(2); 33 | expect(moderatorType.fields).toHaveLength(4); 34 | 35 | const userIdFields = getIdFields(userType); 36 | const groupIdFields = getIdFields(groupType); 37 | const moderatorIdFields = getIdFields(moderatorType); 38 | 39 | expect(userIdFields).toHaveLength(1); 40 | expect(groupIdFields).toHaveLength(1); 41 | expect(moderatorIdFields).toHaveLength(1); 42 | expect(userIdFields[0].name.value).toEqual('userId'); 43 | expect(groupIdFields[0].name.value).toEqual('_id'); 44 | expect(moderatorIdFields[0].name.value).toEqual('moderatorId'); 45 | }); 46 | 47 | test('should define the same ID fields on a type and its input type', () => { 48 | const typeNames = ['User', 'Group', 'Moderator']; 49 | 50 | typeNames.forEach(typeName => { 51 | const type = model.definitions.find( 52 | def => 53 | def.kind === 'ObjectTypeDefinition' && def.name.value === typeName 54 | ); 55 | const inputType = model.definitions.find( 56 | def => 57 | def.kind === 'InputObjectTypeDefinition' && def.name.value === `${typeName}Input` 58 | ); 59 | 60 | const idFields = getIdFields(type); 61 | const inputIdFields = getIdFields(inputType); 62 | 63 | expect(idFields).toHaveLength(1); 64 | expect(inputIdFields).toHaveLength(1); 65 | expect(idFields[0].name.value).toEqual(inputIdFields[0].name.value); 66 | }); 67 | }); 68 | 69 | test('should add CreateInput with nullable ID and UpdateInput with non-nullable ID as mutation input types', () => { 70 | const typeNames = ['User', 'Group']; 71 | 72 | typeNames.forEach(typeName => { 73 | const createInputType = model.definitions.find( 74 | def => 75 | def.kind === 'InputObjectTypeDefinition' && 76 | def.name.value === `${typeName}CreateInput` 77 | ); 78 | 79 | const updateInputType = model.definitions.find( 80 | def => 81 | def.kind === 'InputObjectTypeDefinition' && 82 | def.name.value === `${typeName}UpdateInput` 83 | ); 84 | 85 | expect(createInputType).toBeDefined(); 86 | expect(updateInputType).toBeDefined(); 87 | 88 | const createIdField = getIdFields(createInputType)[0]; 89 | const updateIdField = getIdFields(updateInputType)[0]; 90 | 91 | expect(createIdField.type.kind).toEqual('NamedType'); 92 | expect(updateIdField.type.kind).toEqual('NonNullType'); 93 | }); 94 | }); 95 | 96 | test('should allow enum types as input fields', () => { 97 | const roleEnumType = model.definitions.find(def => def.kind === 'EnumTypeDefinition' && def.name.value === 'Role'); 98 | expect(roleEnumType.values.map(value => value.name.value)).toEqual(expect.arrayContaining(['USER','ADMIN','GUEST'])); 99 | 100 | const userInput = model.definitions.find(def => def.kind === 'InputObjectTypeDefinition' && def.name.value === 'UserInput'); 101 | const userRoleField = userInput.fields.find(field => field.name.value === 'role'); 102 | expect(userRoleField.type.name.value).toEqual('Role'); 103 | }); 104 | 105 | test('should output expected validated schema', () => { 106 | const actual = schemaStringify(model, true); 107 | const expected = readFileSync('./src/test/user-group-validated.graphql', 'utf8') 108 | expect(actual).toBe(expected); 109 | }); 110 | 111 | function getIdFields(objTypeDef) { 112 | return objTypeDef.fields.filter( 113 | field => 114 | field.directives.some(directive => directive.name.value === 'id') 115 | ); 116 | } 117 | }); 118 | -------------------------------------------------------------------------------- /src/test/special-chars-neptune-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "abc!$123&efg", 5 | "properties": [ 6 | { 7 | "name": "instance_type", 8 | "type": "String" 9 | }, 10 | { 11 | "name": "state", 12 | "type": "String" 13 | }, 14 | { 15 | "name": "arn", 16 | "type": "String" 17 | } 18 | ] 19 | }, 20 | { 21 | "label": "abc(123).efg:456", 22 | "properties": [ 23 | { 24 | "name": "name", 25 | "type": "String" 26 | }, 27 | { 28 | "name": "ip_range.first_ip", 29 | "type": "String" 30 | }, 31 | { 32 | "name": "ip_range.last_ip", 33 | "type": "String" 34 | } 35 | ] 36 | }, 37 | { 38 | "label": "abc=123@[efg]456", 39 | "properties": [ 40 | { 41 | "name": "instance_type", 42 | "type": "String" 43 | }, 44 | { 45 | "name": "state", 46 | "type": "String" 47 | }, 48 | { 49 | "name": "arn", 50 | "type": "String" 51 | } 52 | ] 53 | }, 54 | { 55 | "label": "abc{123}|efg-456", 56 | "properties": [ 57 | { 58 | "name": "name", 59 | "type": "String" 60 | }, 61 | { 62 | "name": "ip_range.first_ip", 63 | "type": "String" 64 | }, 65 | { 66 | "name": "ip_range.last_ip", 67 | "type": "String" 68 | } 69 | ] 70 | } 71 | ], 72 | "edgeStructures": [ 73 | { 74 | "label": "resource_link", 75 | "properties": [], 76 | "directions": [ 77 | { 78 | "from": "abc!$123&efg", 79 | "to": "abc(123).efg:456", 80 | "relationship": "MANY-ONE" 81 | }, 82 | { 83 | "from": "abc!$123&efg", 84 | "to": "abc!$123&efg", 85 | "relationship": "ONE-ONE" 86 | } 87 | ] 88 | } 89 | ] 90 | } 91 | -------------------------------------------------------------------------------- /src/test/special-chars.graphql: -------------------------------------------------------------------------------- 1 | type Abc_ex__dol_123_amp_efg @alias(property: "abc!$123&efg") { 2 | _id: ID! @id 3 | instance_type: String 4 | state: String 5 | arn: String 6 | abc_op_123_cp__dot_efg_cn_456Resource_linkOut: Abc_op_123_cp__dot_efg_cn_456 @relationship(edgeType: "resource_link", direction: OUT) 7 | abc_ex__dol_123_amp_efgResource_linkOut: Abc_ex__dol_123_amp_efg @relationship(edgeType: "resource_link", direction: OUT) 8 | abc_ex__dol_123_amp_efgResource_linkIn: Abc_ex__dol_123_amp_efg @relationship(edgeType: "resource_link", direction: IN) 9 | resource_link: Resource_link 10 | } 11 | 12 | input Abc_ex__dol_123_amp_efgInput { 13 | _id: ID @id 14 | instance_type: StringScalarFilters 15 | state: StringScalarFilters 16 | arn: StringScalarFilters 17 | } 18 | 19 | type Abc_op_123_cp__dot_efg_cn_456 @alias(property: "abc(123).efg:456") { 20 | _id: ID! @id 21 | name: String 22 | ip_range_dot_first_ip: String @alias(property: "ip_range.first_ip") 23 | ip_range_dot_last_ip: String @alias(property: "ip_range.last_ip") 24 | abc_ex__dol_123_amp_efgResource_linksIn(filter: Abc_ex__dol_123_amp_efgInput, options: Options): [Abc_ex__dol_123_amp_efg] @relationship(edgeType: "resource_link", direction: IN) 25 | resource_link: Resource_link 26 | } 27 | 28 | input Abc_op_123_cp__dot_efg_cn_456Input { 29 | _id: ID @id 30 | name: StringScalarFilters 31 | ip_range_dot_first_ip: StringScalarFilters @alias(property: "ip_range.first_ip") 32 | ip_range_dot_last_ip: StringScalarFilters @alias(property: "ip_range.last_ip") 33 | } 34 | 35 | type Abc_eq_123_at__os_efg_cs_456 @alias(property: "abc=123@[efg]456") { 36 | _id: ID! @id 37 | instance_type: String 38 | state: String 39 | arn: String 40 | } 41 | 42 | input Abc_eq_123_at__os_efg_cs_456Input { 43 | _id: ID @id 44 | instance_type: StringScalarFilters 45 | state: StringScalarFilters 46 | arn: StringScalarFilters 47 | } 48 | 49 | type Abc_oc_123_cc__vb_efg_hy_456 @alias(property: "abc{123}|efg-456") { 50 | _id: ID! @id 51 | name: String 52 | ip_range_dot_first_ip: String @alias(property: "ip_range.first_ip") 53 | ip_range_dot_last_ip: String @alias(property: "ip_range.last_ip") 54 | } 55 | 56 | input Abc_oc_123_cc__vb_efg_hy_456Input { 57 | _id: ID @id 58 | name: StringScalarFilters 59 | ip_range_dot_first_ip: StringScalarFilters @alias(property: "ip_range.first_ip") 60 | ip_range_dot_last_ip: StringScalarFilters @alias(property: "ip_range.last_ip") 61 | } 62 | 63 | type Resource_link @alias(property: "resource_link") { 64 | _id: ID! @id 65 | } 66 | 67 | input Options { 68 | limit: Int 69 | offset: Int 70 | } 71 | 72 | input StringScalarFilters { 73 | eq: String 74 | contains: String 75 | endsWith: String 76 | startsWith: String 77 | } 78 | 79 | type Query { 80 | getNodeAbc_ex__dol_123_amp_efg(filter: Abc_ex__dol_123_amp_efgInput): Abc_ex__dol_123_amp_efg 81 | getNodeAbc_ex__dol_123_amp_efgs(filter: Abc_ex__dol_123_amp_efgInput, options: Options): [Abc_ex__dol_123_amp_efg] 82 | getNodeAbc_op_123_cp__dot_efg_cn_456(filter: Abc_op_123_cp__dot_efg_cn_456Input): Abc_op_123_cp__dot_efg_cn_456 83 | getNodeAbc_op_123_cp__dot_efg_cn_456s(filter: Abc_op_123_cp__dot_efg_cn_456Input, options: Options): [Abc_op_123_cp__dot_efg_cn_456] 84 | getNodeAbc_eq_123_at__os_efg_cs_456(filter: Abc_eq_123_at__os_efg_cs_456Input): Abc_eq_123_at__os_efg_cs_456 85 | getNodeAbc_eq_123_at__os_efg_cs_456s(filter: Abc_eq_123_at__os_efg_cs_456Input, options: Options): [Abc_eq_123_at__os_efg_cs_456] 86 | getNodeAbc_oc_123_cc__vb_efg_hy_456(filter: Abc_oc_123_cc__vb_efg_hy_456Input): Abc_oc_123_cc__vb_efg_hy_456 87 | getNodeAbc_oc_123_cc__vb_efg_hy_456s(filter: Abc_oc_123_cc__vb_efg_hy_456Input, options: Options): [Abc_oc_123_cc__vb_efg_hy_456] 88 | } 89 | 90 | schema { 91 | query: Query 92 | } 93 | -------------------------------------------------------------------------------- /src/test/unit.test.js.todo: -------------------------------------------------------------------------------- 1 | 2 | import { jest } from '@jest/globals'; 3 | 4 | 5 | //test('main', async () => { 6 | // expect(await mainTest()).not.toBe(null); 7 | //}); 8 | -------------------------------------------------------------------------------- /src/test/user-group-validated.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | userId: ID! @id 3 | firstName: String 4 | lastName: String 5 | role: Role 6 | email: EmailAddress 7 | } 8 | 9 | type Group { 10 | _id: ID! @id 11 | name: String 12 | } 13 | 14 | type Moderator { 15 | moderatorId: ID! @id 16 | name: String 17 | moderates: Group @relationship(type: "GroupEdge", direction: OUT) 18 | groupEdge: GroupEdge 19 | } 20 | 21 | enum Role { 22 | ADMIN 23 | USER 24 | GUEST 25 | } 26 | 27 | "https://the-guild.dev/graphql/scalars/docs/scalars/email-address" 28 | scalar EmailAddress 29 | 30 | input UserInput { 31 | userId: ID! @id 32 | firstName: String 33 | lastName: String 34 | role: Role 35 | email: EmailAddress 36 | } 37 | 38 | input UserCreateInput { 39 | userId: ID @id 40 | firstName: String 41 | lastName: String 42 | role: Role 43 | email: EmailAddress 44 | } 45 | 46 | input UserUpdateInput { 47 | userId: ID! @id 48 | firstName: String 49 | lastName: String 50 | role: Role 51 | email: EmailAddress 52 | } 53 | 54 | input GroupInput { 55 | _id: ID! @id 56 | name: String 57 | } 58 | 59 | input GroupCreateInput { 60 | _id: ID @id 61 | name: String 62 | } 63 | 64 | input GroupUpdateInput { 65 | _id: ID! @id 66 | name: String 67 | } 68 | 69 | input ModeratorInput { 70 | moderatorId: ID! @id 71 | name: String 72 | } 73 | 74 | input ModeratorCreateInput { 75 | moderatorId: ID @id 76 | name: String 77 | } 78 | 79 | input ModeratorUpdateInput { 80 | moderatorId: ID! @id 81 | name: String 82 | } 83 | 84 | type GroupEdge { 85 | _id: ID! @id 86 | } 87 | 88 | input Options { 89 | limit: Int 90 | offset: Int 91 | } 92 | 93 | input StringScalarFilters { 94 | eq: String 95 | contains: String 96 | endsWith: String 97 | startsWith: String 98 | } 99 | 100 | type Query { 101 | getNodeUser(filter: UserInput): User 102 | getNodeUsers(filter: UserInput, options: Options): [User] 103 | getNodeGroup(filter: GroupInput): Group 104 | getNodeGroups(filter: GroupInput, options: Options): [Group] 105 | getNodeModerator(filter: ModeratorInput): Moderator 106 | getNodeModerators(filter: ModeratorInput, options: Options): [Moderator] 107 | } 108 | 109 | type Mutation { 110 | createNodeUser(input: UserCreateInput!): User 111 | updateNodeUser(input: UserUpdateInput!): User 112 | deleteNodeUser(userId: ID!): Boolean 113 | createNodeGroup(input: GroupCreateInput!): Group 114 | updateNodeGroup(input: GroupUpdateInput!): Group 115 | deleteNodeGroup(_id: ID!): Boolean 116 | createNodeModerator(input: ModeratorCreateInput!): Moderator 117 | updateNodeModerator(input: ModeratorUpdateInput!): Moderator 118 | deleteNodeModerator(moderatorId: ID!): Boolean 119 | connectNodeModeratorToNodeGroupEdgeGroupEdge(from_id: ID!, to_id: ID!): GroupEdge 120 | deleteEdgeGroupEdgeFromModeratorToGroup(from_id: ID!, to_id: ID!): Boolean 121 | } 122 | 123 | schema { 124 | query: Query 125 | mutation: Mutation 126 | } -------------------------------------------------------------------------------- /src/test/user-group.graphql: -------------------------------------------------------------------------------- 1 | type User { 2 | userId: ID! @id 3 | firstName: String 4 | lastName: String 5 | role: Role 6 | email: EmailAddress 7 | } 8 | 9 | type Group { 10 | name: String 11 | } 12 | 13 | type Moderator { 14 | moderatorId: ID! 15 | name: String 16 | moderates: Group 17 | } 18 | 19 | enum Role { 20 | ADMIN 21 | USER 22 | GUEST 23 | } 24 | 25 | "https://the-guild.dev/graphql/scalars/docs/scalars/email-address" 26 | scalar EmailAddress 27 | -------------------------------------------------------------------------------- /src/test/util-promise.test.js: -------------------------------------------------------------------------------- 1 | import { mapAll } from '../util-promise.js' 2 | 3 | describe('mapAll', () => { 4 | test('should map each element, resolve each Promise, and resolve to the mapped values', async () => { 5 | await expect(mapAll([1, 2, 3, 4, 5, 6, 7, 8], double)) 6 | .resolves 7 | .toEqual([2, 4, 6, 8, 10, 12, 14, 16]); 8 | }); 9 | 10 | test('should map each element in batches', async () => { 11 | const count = 100; 12 | const batchSize = 50; 13 | const batchCounter = makeBatchCounter(count); 14 | 15 | await expect(mapAll(Array(count + 1).fill(0), (i) => batchCounter.fn(i), batchSize)) 16 | .rejects 17 | .toThrow(Error); 18 | expect(batchCounter.count).toEqual(count); 19 | }); 20 | 21 | test('should reject with the same error as a rejected mapped Promise', async () => { 22 | await expect(mapAll([2, 4, 6, 3, 8, 10], assertEven)) 23 | .rejects 24 | .toThrow(OddNumberError); 25 | }); 26 | 27 | test('should resolve to an empty array for empty input arrays', async () => { 28 | await expect(mapAll([], Promise.resolve)).resolves.toEqual([]); 29 | }); 30 | 31 | test('should resolve to an empty array for non-positive batch sizes', async () => { 32 | await expect(mapAll([1, 2, 3], Promise.resolve, -1)).resolves.toEqual([]); 33 | await expect(mapAll([1, 2, 3], Promise.resolve, 0)).resolves.toEqual([]); 34 | }); 35 | 36 | function double(i) { 37 | return Promise.resolve(i * 2); 38 | } 39 | 40 | function makeBatchCounter(n) { 41 | return { 42 | count: 0, 43 | fn: function (i) { 44 | if (this.count >= n) { 45 | throw new Error(); 46 | } 47 | this.count++; 48 | return Promise.resolve(i); 49 | } 50 | }; 51 | } 52 | 53 | function assertEven(i) { 54 | if (i % 2 !== 0) { 55 | throw new OddNumberError(); 56 | } 57 | 58 | return Promise.resolve(i); 59 | } 60 | 61 | class OddNumberError extends Error {} 62 | }); 63 | -------------------------------------------------------------------------------- /src/test/util.test.js: -------------------------------------------------------------------------------- 1 | import {parseNeptuneDomainFromHost, parseNeptuneEndpoint} from '../util.js'; 2 | 3 | test('parse domain from neptune cluster host', () => { 4 | expect(parseNeptuneDomainFromHost('db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com')) 5 | .toBe('neptune.amazonaws.com'); 6 | }); 7 | 8 | test('parse domain from neptune analytics host', () => { 9 | expect(parseNeptuneDomainFromHost('g-abcdef.us-west-2.neptune-graph.amazonaws.com')) 10 | .toBe('neptune-graph.amazonaws.com'); 11 | }); 12 | 13 | test('parse domain from host without enough parts throws error', () => { 14 | expect(() => parseNeptuneDomainFromHost('invalid.com')) 15 | .toThrow('Cannot parse neptune host invalid.com because it has 2 part(s) delimited by . but expected at least 5'); 16 | }); 17 | 18 | test('parse neptune db endpoint', () => { 19 | let neptuneInfo = parseNeptuneEndpoint('db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com:8182'); 20 | expect(neptuneInfo).toHaveProperty('port', '8182'); 21 | expect(neptuneInfo).toHaveProperty('host', 'db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com'); 22 | expect(neptuneInfo).toHaveProperty('domain', 'neptune.amazonaws.com'); 23 | expect(neptuneInfo).toHaveProperty('region', 'us-west-2'); 24 | expect(neptuneInfo).toHaveProperty('graphName', 'db-neptune-abc-def'); 25 | expect(neptuneInfo).toHaveProperty('neptuneType', 'neptune-db'); 26 | }); 27 | 28 | test('parse neptune analytics endpoint', () => { 29 | let neptuneInfo = parseNeptuneEndpoint('g-abcdef.us-east-1.neptune-graph.amazonaws.com:8183'); 30 | expect(neptuneInfo).toHaveProperty('port', '8183'); 31 | expect(neptuneInfo).toHaveProperty('host', 'g-abcdef.us-east-1.neptune-graph.amazonaws.com'); 32 | expect(neptuneInfo).toHaveProperty('domain', 'neptune-graph.amazonaws.com'); 33 | expect(neptuneInfo).toHaveProperty('region', 'us-east-1'); 34 | expect(neptuneInfo).toHaveProperty('graphName', 'g-abcdef'); 35 | expect(neptuneInfo).toHaveProperty('neptuneType', 'neptune-graph'); 36 | }); 37 | 38 | test('parse neptune endpoint without port throws error', () => { 39 | expect(() => parseNeptuneEndpoint('db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com')) 40 | .toThrow('Cannot parse neptune endpoint db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com because it is not in expected format of host:port'); 41 | }); 42 | 43 | test('parse neptune endpoint not enough parts in domain throws error', () => { 44 | expect(() => parseNeptuneEndpoint('invalid.com:1234')) 45 | .toThrow('Cannot parse neptune host invalid.com because it has 2 part(s) delimited by . but expected at least 5'); 46 | }); -------------------------------------------------------------------------------- /src/util-promise.js: -------------------------------------------------------------------------------- 1 | const BATCH_SIZE = 20; 2 | 3 | /** 4 | * @callback MapCallback 5 | * @param {T} value 6 | * @param {number} index 7 | * @param {T[]} array 8 | * @returns {U} 9 | * @template T, U 10 | */ 11 | 12 | /** 13 | * Calls a Promise-returning callback function on each element of an array and creates a Promise 14 | * that is resolved with an array of results when all of the returned Promises resolve, or rejected 15 | * when any Promise is rejected. 16 | * 17 | * The elements of the array are mapped and then resolved in batches of size `batchSize`, ensuring 18 | * that no more than `batchSize` Promises are pending at one time. This is useful for e.g. avoiding 19 | * too many simultaneous web requests. 20 | * 21 | * @param {T[]} array an array to map over 22 | * @param {MapCallback>} callbackfn a function that accepts up to three arguments. The 23 | * mapAll function calls the callbackfn function one time for each element in the array 24 | * @param {number} batchSize the number of Promises to concurrently resolve 25 | * @returns {Promise} 26 | * @template T, U 27 | */ 28 | async function mapAll(array, callbackfn, batchSize = BATCH_SIZE) { 29 | if (batchSize <= 0) { 30 | return []; 31 | } 32 | 33 | const results = []; 34 | 35 | for (let i = 0; i < array.length; i += batchSize) { 36 | const promises = array.slice(i, i + batchSize).map(callbackfn); 37 | const batchResults = await Promise.all(promises); 38 | results.push(...batchResults); 39 | } 40 | 41 | return results; 42 | } 43 | 44 | export { mapAll }; 45 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | Licensed under the Apache License, Version 2.0 (the "License"). 4 | You may not use this file except in compliance with the License. 5 | A copy of the License is located at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | or in the "license" file accompanying this file. This file is distributed 8 | on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 9 | express or implied. See the License for the specific language governing 10 | permissions and limitations under the License. 11 | */ 12 | 13 | const MIN_HOST_PARTS = 5; 14 | const NUM_DOMAIN_PARTS = 3; 15 | const HOST_DELIMITER = '.'; 16 | const NEPTUNE_GRAPH = 'neptune-graph'; 17 | const NEPTUNE_DB = 'neptune-db'; 18 | 19 | /** 20 | * Splits a neptune host into its parts, throwing an Error if there are unexpected number of parts. 21 | * 22 | * @param neptuneHost 23 | */ 24 | function splitHost(neptuneHost) { 25 | let parts = neptuneHost.split(HOST_DELIMITER); 26 | if (parts.length < MIN_HOST_PARTS) { 27 | throw Error('Cannot parse neptune host ' + neptuneHost + ' because it has ' + parts.length + 28 | ' part(s) delimited by ' + HOST_DELIMITER + ' but expected at least ' + MIN_HOST_PARTS); 29 | } 30 | return parts; 31 | } 32 | 33 | function getDomainFromHostParts(hostParts) { 34 | // last 3 parts of the host make up the domain 35 | // ie. neptune.amazonaws.com or neptune-graph.amazonaws.com 36 | let domainParts = hostParts.splice(hostParts.length - NUM_DOMAIN_PARTS, NUM_DOMAIN_PARTS); 37 | return domainParts.join(HOST_DELIMITER); 38 | } 39 | 40 | /** 41 | * Parses the domain from the given neptune db or neptune analytics host. 42 | * 43 | * Example: g-abcdef.us-west-2.neptune-graph.amazonaws.com ==> neptune-graph.amazonaws.com 44 | * Example: db-neptune-abc-def.cluster-xyz.us-west-2.neptune.amazonaws.com ==> neptune.amazonaws.com 45 | * 46 | * @param neptuneHost 47 | */ 48 | function parseNeptuneDomainFromHost(neptuneHost) { 49 | return getDomainFromHostParts(splitHost(neptuneHost)); 50 | } 51 | 52 | /** 53 | * Parses a neptune endpoint into its parts. 54 | * 55 | * @param neptuneEndpoint 56 | * @returns {{graphName: (string), port: (string), domain: (string), neptuneType: (string), host: (string), region: (string)}} 57 | */ 58 | function parseNeptuneEndpoint(neptuneEndpoint) { 59 | let endpointParts = neptuneEndpoint.split(':'); 60 | if (endpointParts.length !== 2) { 61 | throw Error('Cannot parse neptune endpoint ' + neptuneEndpoint + ' because it is not in expected format of host:port'); 62 | } 63 | 64 | const host = endpointParts[0]; 65 | const hostParts = splitHost(host); 66 | const domain = getDomainFromHostParts(hostParts); 67 | const neptuneType = domain.includes(NEPTUNE_GRAPH) ? NEPTUNE_GRAPH : NEPTUNE_DB; 68 | const region = neptuneType === NEPTUNE_DB ? hostParts[2] : hostParts[1]; 69 | 70 | return { 71 | port: endpointParts[1], 72 | host: host, 73 | domain: domain, 74 | region: region, 75 | graphName: hostParts[0], 76 | neptuneType: neptuneType 77 | }; 78 | } 79 | 80 | export {parseNeptuneDomainFromHost, parseNeptuneEndpoint}; -------------------------------------------------------------------------------- /src/zipPackage.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import archiver from 'archiver'; 3 | import path from "path"; 4 | import {fileURLToPath} from "url"; 5 | 6 | /** 7 | * Creates a zip file with specified contents. 8 | * 9 | * @param {string} targetZipFilePath path to where the zip should be created 10 | * @param {object[]} includePaths paths to files or folders that should be included in the zip 11 | * @param {string} includePaths.source path to the source file or folder 12 | * @param {string} includePaths.target name of the file or folder to create in the zip. If it is a file and not provided, the source file name will be used. If it is a folder and not provided, the contents will be created in the root of the zip 13 | * @param {object[]} includeContent additional string contents that should be included in the zip 14 | * @param {string} includeContent.source string content to include in the zip as a file 15 | * @param {string} includeContent.target name of the target file to create in the zip that will contain the source content 16 | * @param {string[]} excludePatterns if any includePaths are a folder, patterns of files to exclude from the folder 17 | * @returns {Promise} 18 | */ 19 | async function createZip({targetZipFilePath, includePaths = [], includeContent = [], excludePatterns = []}) { 20 | const output = fs.createWriteStream(targetZipFilePath); 21 | const archive = archiver('zip', {zlib: {level: 9}}); 22 | archive.pipe(output); 23 | 24 | includePaths.forEach(includePath => { 25 | const stats = fs.lstatSync(includePath.source); 26 | if (stats.isDirectory()) { 27 | archive.glob('**/*', { 28 | cwd: includePath.source, 29 | ignore: excludePatterns || [], 30 | dot: false, // exclude hidden dotfiles 31 | }, { 32 | // if no target specified, add contents to root of archive 33 | prefix: includePath.target || '' 34 | }); 35 | } else { 36 | archive.file(includePath.source, {name: includePath.target ?? path.basename(includePath.source)}) 37 | } 38 | }) 39 | includeContent.forEach(content => { 40 | archive.append(content.source, {name: content.target}); 41 | }); 42 | await archive.finalize(); 43 | } 44 | 45 | /** 46 | * Creates a lambda deployment ZIP package 47 | * 48 | * @param outputZipFilePath the path to where the zip should be created 49 | * @param templateFolderPath the path to the template folder that contains contents to add to the zip 50 | * @param resolverFilePath the path to the resolver file that should be added to the zip 51 | * @param resolverSchemaFilePath the path to the resolver schema file that should be added to the zip 52 | * @returns {Promise>} 53 | */ 54 | export async function createLambdaDeploymentPackage({outputZipFilePath, templateFolderPath, resolverFilePath, resolverSchemaFilePath}) { 55 | const filePaths = [{source: templateFolderPath}, {source: resolverFilePath, target: 'output.resolver.graphql.js'}]; 56 | if (templateFolderPath.includes('HTTP')) { 57 | filePaths.push({ 58 | source: path.join(getModulePath(), '/../templates/queryHttpNeptune.mjs') 59 | }) 60 | } 61 | 62 | filePaths.push({ 63 | source: resolverSchemaFilePath, 64 | target: 'output.resolver.schema.json' 65 | }) 66 | 67 | await createZip({ 68 | targetZipFilePath: outputZipFilePath, 69 | includePaths: filePaths 70 | }); 71 | } 72 | 73 | /** 74 | * Creates a zip package of Apollo Server deployment artifacts. 75 | * 76 | * @param zipFilePath the file path where the zip should be created 77 | * @param resolverFilePath path to the resolver file to include in the zip 78 | * @param schemaFilePath path to the schema file to include in the zip 79 | * @param resolverSchemaFilePath path to the resolver schema file to include in the zip 80 | * @param neptuneInfo object containing neptune db/graph related information such as URL, region, etc 81 | * @param isSubgraph true if the service should be deployed as a subgraph 82 | * @returns {Promise} 83 | */ 84 | export async function createApolloDeploymentPackage({zipFilePath, resolverFilePath, schemaFilePath, resolverSchemaFilePath, neptuneInfo, isSubgraph = false}) { 85 | const envVars = [ 86 | `NEPTUNE_TYPE=${neptuneInfo.neptuneType}`, 87 | `NEPTUNE_HOST=${neptuneInfo.host}`, 88 | `NEPTUNE_PORT=${neptuneInfo.port}`, 89 | `AWS_REGION=${neptuneInfo.region}`, 90 | 'LOGGING_ENABLED=false', // do not log query data by default 91 | `SUBGRAPH=${isSubgraph}` 92 | ]; 93 | const modulePath = getModulePath(); 94 | await createZip({ 95 | targetZipFilePath: zipFilePath, 96 | includePaths: [ 97 | {source: path.join(modulePath, '/../templates/ApolloServer')}, 98 | {source: resolverFilePath, target: 'output.resolver.graphql.js'}, 99 | {source: schemaFilePath, target: 'output.schema.graphql'}, 100 | {source: resolverSchemaFilePath, target: 'output.resolver.schema.json'}, 101 | 102 | // querying neptune using SDK not yet supported 103 | {source: path.join(modulePath, '/../templates/queryHttpNeptune.mjs')} 104 | ], 105 | includeContent: [{source: envVars.join('\n'), target: '.env'}], 106 | // exclude node_modules from apollo package 107 | excludePatterns: ['**/node_modules/**'] 108 | }) 109 | } 110 | 111 | export function getModulePath() { 112 | return path.dirname(fileURLToPath(import.meta.url)); 113 | } 114 | -------------------------------------------------------------------------------- /templates/ApolloServer/index.mjs: -------------------------------------------------------------------------------- 1 | import {ApolloServer} from '@apollo/server'; 2 | import {startStandaloneServer} from '@apollo/server/standalone'; 3 | import {buildSubgraphSchema} from '@apollo/subgraph'; 4 | import {readFileSync} from 'fs'; 5 | import {gql} from 'graphql-tag'; 6 | import {resolveEvent} from './neptune.mjs' 7 | import dotenv from 'dotenv'; 8 | 9 | dotenv.config(); 10 | 11 | const typeDefs = gql(readFileSync('./output.schema.graphql', 'utf-8')); 12 | const queryDefinition = typeDefs.definitions.find( 13 | definition => definition.kind === 'ObjectTypeDefinition' && definition.name.value === 'Query' 14 | ); 15 | const queryNames = queryDefinition ? queryDefinition.fields.map(field => field.name.value) : []; 16 | 17 | const mutationDefinition = typeDefs.definitions.find( 18 | definition => definition.kind === 'ObjectTypeDefinition' && definition.name.value === 'Mutation' 19 | ); 20 | const mutationNames = mutationDefinition ? mutationDefinition.fields.map(field => field.name.value) : []; 21 | 22 | /** 23 | * Resolves GraphQL queries and mutations by processing the info object and arguments. 24 | * 25 | * @param {Object} info - GraphQLResolveInfo object 26 | * @param {Object} args - graphQL query arguments 27 | * @returns {Promise<*>} Resolved data from the event processor 28 | */ 29 | function resolve(info, args) { 30 | if (!info?.fieldName) { 31 | throw new Error('Missing fieldName on GraphQLResolveInfo'); 32 | } 33 | if (!Array.isArray(info.fieldNodes) || info.fieldNodes.length !== 1) { 34 | throw new Error('Invalid fieldNodes on GraphQLResolveInfo'); 35 | } 36 | const event = { 37 | field: info.fieldName, 38 | arguments: args, 39 | selectionSet: info.fieldNodes[0].selectionSet, 40 | variables: info.variableValues, 41 | fragments: info.fragments 42 | }; 43 | 44 | return resolveEvent(event).then((result) => { 45 | return result; 46 | }); 47 | } 48 | 49 | const resolvers = { 50 | Query: queryNames.reduce((accumulator, queryName) => { 51 | accumulator[queryName] = (parent, args, context, info) => { 52 | return resolve(info, args); 53 | }; 54 | return accumulator; 55 | }, {}), 56 | 57 | Mutation: mutationNames.reduce((accumulator, mutationName) => { 58 | accumulator[mutationName] = (parent, args, context, info) => { 59 | return resolve(info, args); 60 | }; 61 | return accumulator; 62 | }, {}), 63 | }; 64 | 65 | const server = process.env.SUBGRAPH === 'true' ? new ApolloServer({ 66 | schema: buildSubgraphSchema([{ 67 | typeDefs, 68 | resolvers 69 | }]) 70 | }) : new ApolloServer({typeDefs, resolvers}); 71 | 72 | const {url} = await startStandaloneServer(server); 73 | console.log(`🚀 Server ready at ${url}`); 74 | -------------------------------------------------------------------------------- /templates/ApolloServer/neptune.mjs: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import * as rax from 'retry-axios'; 3 | import {aws4Interceptor} from 'aws4-axios'; 4 | import {fromNodeProviderChain} from '@aws-sdk/credential-providers'; 5 | import dotenv from 'dotenv'; 6 | import {queryNeptune} from './queryHttpNeptune.mjs' 7 | import {resolveGraphDBQueryFromEvent, initSchema} from "./output.resolver.graphql.js"; 8 | import {readFileSync} from "fs"; 9 | 10 | 11 | dotenv.config(); 12 | 13 | const loggingEnabled = process.env.LOGGING_ENABLED === 'true'; 14 | const credentialProvider = fromNodeProviderChain(); 15 | const credentials = await credentialProvider(); 16 | const interceptor = aws4Interceptor({ 17 | options: { 18 | region: process.env.AWS_REGION, 19 | service: process.env.NEPTUNE_TYPE, 20 | }, 21 | credentials: credentials 22 | }); 23 | axios.interceptors.request.use(interceptor); 24 | rax.attach(); 25 | 26 | export async function resolveEvent(event) { 27 | const schemaDataModelJSON = readFileSync('output.resolver.schema.json', 'utf-8'); 28 | let schemaModel = JSON.parse(schemaDataModelJSON); 29 | initSchema(schemaModel); 30 | const resolved = resolveGraphDBQueryFromEvent(event); 31 | try { 32 | return queryNeptune(`https://${process.env.NEPTUNE_HOST}:${process.env.NEPTUNE_PORT}`, resolved, {loggingEnabled: loggingEnabled}); 33 | } catch (err) { 34 | if (loggingEnabled) { 35 | console.error(err); 36 | } 37 | return { 38 | "error": [{"message": err}] 39 | }; 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /templates/ApolloServer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neptune-apollo-server", 3 | "version": "1.2.0", 4 | "description": "", 5 | "license": "Apache-2.0", 6 | "author": "", 7 | "type": "module", 8 | "main": "index.mjs", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "dependencies": { 13 | "@apollo/server": "^4.11.3", 14 | "@apollo/subgraph": "^2.9.3", 15 | "@aws-sdk/credential-providers": "^3.729.0", 16 | "aws4-axios": "^3.3.14", 17 | "axios": "^1.7.9", 18 | "graphql-tag": "^2.12.6", 19 | "retry-axios": "^3.1.3", 20 | "dotenv": "^16.4.7" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /templates/Lambda4AppSyncGraphSDK/index.mjs: -------------------------------------------------------------------------------- 1 | import {ExecuteQueryCommand, NeptuneGraphClient} from "@aws-sdk/client-neptune-graph"; 2 | import {resolveGraphDBQueryFromAppSyncEvent, initSchema} from './output.resolver.graphql.js'; 3 | import {readFileSync} from "fs"; 4 | 5 | const PROTOCOL = 'https'; 6 | const QUERY_LANGUAGE = 'OPEN_CYPHER'; 7 | const RESOLVER_LANGUAGE = 'opencypher'; 8 | 9 | let client; 10 | 11 | function getClient() { 12 | if (!client) { 13 | try { 14 | log('Instantiating NeptuneGraphClient') 15 | client = new NeptuneGraphClient({ 16 | port: process.env.NEPTUNE_PORT, 17 | host: process.env.NEPTUNE_DOMAIN, 18 | region: process.env.NEPTUNE_REGION, 19 | protocol: PROTOCOL, 20 | }); 21 | } catch (error) { 22 | return onError('Error instantiating NeptuneGraphClient: ', error); 23 | } 24 | } 25 | return client; 26 | } 27 | 28 | function onError(context, error) { 29 | let msg; 30 | if (error) { 31 | msg = context + ':' + error.message; 32 | } else { 33 | msg = context; 34 | } 35 | console.error(msg); 36 | if (error) { 37 | throw error; 38 | } 39 | throw new Error(msg); 40 | } 41 | 42 | function log(message) { 43 | if (process.env.LOGGING_ENABLED) { 44 | console.log(message); 45 | } 46 | } 47 | 48 | /** 49 | * Converts graphQL query to open cypher. 50 | */ 51 | function resolveGraphQuery(event) { 52 | try { 53 | const schemaDataModelJSON = readFileSync('output.resolver.schema.json', 'utf-8'); 54 | let schemaModel = JSON.parse(schemaDataModelJSON); 55 | initSchema(schemaModel); 56 | let resolver = resolveGraphDBQueryFromAppSyncEvent(event); 57 | if (resolver.language !== RESOLVER_LANGUAGE) { 58 | return onError('Unsupported resolver language:' + resolver.language) 59 | } 60 | log('Resolved ' + resolver.language + ' query successfully'); 61 | return resolver; 62 | } catch (error) { 63 | return onError('Error resolving graphQL query', error); 64 | } 65 | } 66 | 67 | /** 68 | * Converts incoming graphQL query into open cypher format and sends the query to neptune analytics query API. 69 | */ 70 | export const handler = async (event) => { 71 | if (event.selectionSetGraphQL.includes('...')) { 72 | throw new Error('Fragments are not supported'); 73 | } 74 | let resolver = resolveGraphQuery(event); 75 | 76 | try { 77 | const command = new ExecuteQueryCommand({ 78 | graphIdentifier: process.env.NEPTUNE_DB_NAME, 79 | queryString: resolver.query, 80 | language: QUERY_LANGUAGE, 81 | parameters: resolver.parameters 82 | }); 83 | const response = await getClient().send(command); 84 | log('Received query response'); 85 | let data = await new Response(response.payload).json(); 86 | // query result should have result array of single item or an empty array 87 | // {"results": [{ ... }]} 88 | if (data.results.length === 0) { 89 | log('Query produced no results'); 90 | return []; 91 | } 92 | if (data.results.length !== 1) { 93 | return onError('Expected 1 query result but received ' + data.results.length); 94 | } 95 | log('Obtained data from query response'); 96 | return data.results[0][Object.keys(data.results[0])[0]]; 97 | } catch (error) { 98 | return onError('Error executing ' + QUERY_LANGUAGE + ' query: ', error); 99 | } 100 | }; -------------------------------------------------------------------------------- /templates/Lambda4AppSyncGraphSDK/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda4appsyncgraphsdk", 3 | "version": "1.2.0", 4 | "description": "AWS Lambda function to bridge AppSync to Neptune Analytics", 5 | "main": "index.mjs", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "AWS", 11 | "license": "Apache-2.0", 12 | "dependencies": { 13 | "@aws-sdk/client-neptune-graph": "3.662.0", 14 | "graphql-tag": "2.12.6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/Lambda4AppSyncHTTP/index.mjs: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import * as rax from 'retry-axios'; 3 | import { aws4Interceptor } from "aws4-axios"; 4 | import {resolveGraphDBQueryFromAppSyncEvent, initSchema} from './output.resolver.graphql.js'; 5 | import {queryNeptune} from "./queryHttpNeptune.mjs"; 6 | import {readFileSync} from "fs"; 7 | 8 | const LOGGING_ENABLED = process.env.LOGGING_ENABLED; 9 | 10 | const { 11 | AWS_ACCESS_KEY_ID, 12 | AWS_SECRET_ACCESS_KEY, 13 | AWS_SESSION_TOKEN, 14 | AWS_REGION 15 | } = process.env; 16 | 17 | 18 | if (process.env.NEPTUNE_IAM_AUTH_ENABLED === 'true') { 19 | let serviceName; 20 | if (process.env.NEPTUNE_TYPE) { 21 | serviceName = process.env.NEPTUNE_TYPE; 22 | } else { 23 | console.log('NEPTUNE_TYPE environment variable is not set - defaulting to neptune-db'); 24 | serviceName = 'neptune-db'; 25 | } 26 | const interceptor = aws4Interceptor({ 27 | options: { 28 | region: AWS_REGION, 29 | service: serviceName, 30 | }, 31 | credentials: { 32 | accessKeyId: AWS_ACCESS_KEY_ID, 33 | secretAccessKey: AWS_SECRET_ACCESS_KEY, 34 | sessionToken: AWS_SESSION_TOKEN 35 | } 36 | }); 37 | 38 | axios.interceptors.request.use(interceptor); 39 | } 40 | 41 | rax.attach(); 42 | 43 | export const handler = async (event) => { 44 | if (LOGGING_ENABLED) console.log("Event: ", event); 45 | if (event.selectionSetGraphQL.includes('...')) { 46 | throw new Error('Fragments are not supported'); 47 | } 48 | try { 49 | // Create Neptune query from GraphQL query 50 | const schemaDataModelJSON = readFileSync('output.resolver.schema.json', 'utf-8'); 51 | let schemaModel = JSON.parse(schemaDataModelJSON); 52 | initSchema(schemaModel); 53 | const resolver = resolveGraphDBQueryFromAppSyncEvent(event); 54 | if (LOGGING_ENABLED) console.log("Resolver: ", resolver); 55 | return queryNeptune(`https://${process.env.NEPTUNE_HOST}:${process.env.NEPTUNE_PORT}`, resolver, {loggingEnabled: LOGGING_ENABLED}) 56 | } catch (err) { 57 | console.error(err); 58 | throw err; 59 | } 60 | 61 | }; -------------------------------------------------------------------------------- /templates/Lambda4AppSyncHTTP/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda4appsynchttp", 3 | "version": "1.2.0", 4 | "description": "AWS Lambda function to bridge AppSync to Neptune or Neptune Analytics", 5 | "main": "index.mjs", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "aws4-axios": "3.3.0", 14 | "axios": "^1.9.0", 15 | "graphql-tag": "2.12.6", 16 | "retry-axios": "3.1.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /templates/Lambda4AppSyncSDK/index.mjs: -------------------------------------------------------------------------------- 1 | import { NeptunedataClient, ExecuteOpenCypherQueryCommand, ExecuteGremlinQueryCommand } from "@aws-sdk/client-neptunedata"; 2 | import {resolveGraphDBQueryFromAppSyncEvent, refactorGremlinqueryOutput, initSchema} from './output.resolver.graphql.js'; 3 | import {readFileSync} from "fs"; 4 | 5 | const LOGGING_ENABLED = process.env.LOGGING_ENABLED; 6 | 7 | const config = { 8 | endpoint: `https://${process.env.NEPTUNE_HOST}:${process.env.NEPTUNE_PORT}` 9 | }; 10 | 11 | let client; 12 | 13 | function getClient() { 14 | if (client) { 15 | return client; 16 | } 17 | 18 | try { 19 | client = new NeptunedataClient(config); 20 | return client; 21 | } catch (error) { 22 | onError('new NeptunedataClient: ', error); 23 | } 24 | } 25 | 26 | 27 | function onError (location, error) { 28 | console.error(location, ': ', error.message); 29 | throw error; 30 | } 31 | 32 | 33 | export const handler = async(event) => { 34 | let r = null; 35 | let result = null; 36 | 37 | if (LOGGING_ENABLED) console.log(event); 38 | if (event.selectionSetGraphQL.includes('...')) { 39 | throw new Error('Fragments are not supported'); 40 | } 41 | 42 | let resolver = { query:'', parameters:{}, language: 'opencypher', fieldsAlias: {} }; 43 | 44 | try { 45 | const schemaDataModelJSON = readFileSync('output.resolver.schema.json', 'utf-8'); 46 | let schemaModel = JSON.parse(schemaDataModelJSON); 47 | initSchema(schemaModel); 48 | resolver = resolveGraphDBQueryFromAppSyncEvent(event); 49 | if (LOGGING_ENABLED) console.log(JSON.stringify(resolver, null, 2)); 50 | } catch (error) { 51 | onError('Resolver: ', error); 52 | } 53 | 54 | if (resolver.language == 'gremlin') { 55 | try { 56 | const input = { 57 | gremlinQuery: resolver.query 58 | }; 59 | const command = new ExecuteGremlinQueryCommand(input); 60 | const response = await getClient().send(command); 61 | result = response["result"]["data"]; 62 | const refac = refactorGremlinqueryOutput(result, resolver.fieldsAlias) 63 | r = JSON.parse(refac); 64 | } catch (error) { 65 | onError('Gremlin query: ', error); 66 | } 67 | } 68 | 69 | if (resolver.language == 'opencypher') { 70 | try { 71 | const input = { 72 | openCypherQuery: resolver.query, 73 | parameters: JSON.stringify(resolver.parameters) 74 | }; 75 | const command = new ExecuteOpenCypherQueryCommand(input); 76 | const response = await getClient().send(command); 77 | result = response.results; 78 | r = result[0][Object.keys(result[0])]; 79 | } catch (error) { 80 | onError('openCypher query: ', error); 81 | } 82 | } 83 | 84 | return r; 85 | }; -------------------------------------------------------------------------------- /templates/Lambda4AppSyncSDK/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda4appsyncsdk", 3 | "version": "1.2.0", 4 | "description": "AWS Lambda function to bridge AppSync to Neptune", 5 | "main": "index.mjs", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "AWS", 11 | "license": "Apache-2.0", 12 | "dependencies": { 13 | "@aws-sdk/client-neptunedata": "3.403.0", 14 | "graphql-tag": "2.12.6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/queryHttpNeptune.mjs: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import * as rax from 'retry-axios'; 3 | import {refactorGremlinqueryOutput} from './output.resolver.graphql.js' 4 | 5 | /** 6 | * Sends an opencypher or gremlin query via HTTP to neptune and transforms the response into a graphQL json response. 7 | * 8 | * It is expected that the axios http client authentication should be pre-configured before calling this function. 9 | * 10 | * @param {string} neptuneUrl the neptune URL to query 11 | * @param {object} resolvedQuery query object that contains query data 12 | * @param {string} resolvedQuery.query the query string 13 | * @param {string} resolvedQuery.language the query language - either opencypher or gremlin 14 | * @param {object} resolvedQuery.parameters the query parameters object 15 | * @param {object} resolvedQuery.fieldsAlias alias for the query field - applies only for gremlin queries 16 | * @param {object} options optional options object 17 | * @param {boolean} options.loggingEnabled true if non-error logging should be enabled 18 | * @returns {Promise<{error: [{message}]}|*|null>} 19 | */ 20 | export async function queryNeptune(neptuneUrl, resolvedQuery, options = {loggingEnabled: false}) { 21 | const loggingEnabled = options?.loggingEnabled; 22 | 23 | try { 24 | const requestConfig = { 25 | raxConfig: { 26 | retry: 5, noResponseRetries: 5, onRetryAttempt: err => { 27 | const cfg = rax.getConfig(err); 28 | if (loggingEnabled) { 29 | console.log(`Retry attempt #${cfg.currentRetryAttempt} Status: ${err.response.statusText}`); 30 | } 31 | } 32 | }, timeout: 20000 33 | }; 34 | 35 | let response; 36 | if (loggingEnabled) { 37 | console.log("Query: ", JSON.stringify(resolvedQuery, null, 2)); 38 | } 39 | if (resolvedQuery.language === 'opencypher') { 40 | response = await axios.post(`${neptuneUrl}/opencypher`, { 41 | query: resolvedQuery.query, parameters: JSON.stringify(resolvedQuery.parameters) 42 | }, requestConfig); 43 | } else { 44 | response = await axios.post(`${neptuneUrl}/gremlin`, { 45 | gremlin: resolvedQuery.query 46 | }, requestConfig); 47 | } 48 | if (loggingEnabled) { 49 | console.log("Query result: ", JSON.stringify(response.data, null, 2)); 50 | } 51 | 52 | if (resolvedQuery.language === 'gremlin') { 53 | const gremlinData = response.data["result"]["data"]; 54 | const jsonData = refactorGremlinqueryOutput(gremlinData, resolvedQuery.fieldsAlias); 55 | if (loggingEnabled) { 56 | console.log("Gremlin query json data: ", jsonData); 57 | } 58 | return JSON.parse(jsonData); 59 | } 60 | 61 | if (resolvedQuery.language === 'opencypher') { 62 | let openCypherData = response.data; 63 | if (openCypherData.results.length === 0) { 64 | // this happens if a query for a single entity using match clause with limit 1 does not find any result 65 | return null; 66 | } 67 | let jsonData = openCypherData.results[0][Object.keys(openCypherData.results[0])[0]]; 68 | if (loggingEnabled) { 69 | console.log("Opencypher query json data: ", jsonData); 70 | } 71 | return jsonData; 72 | } 73 | } catch (err) { 74 | console.error("Failed to query neptune") 75 | console.error(err); 76 | return { 77 | "error": [{"message": err}] 78 | }; 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/Case01.01.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { jest } from '@jest/globals'; 3 | import { readJSONFile } from '../../testLib'; 4 | import { main } from "../../../src/main"; 5 | 6 | const casetest = readJSONFile('./test/TestCases/Case01/case.json'); 7 | 8 | async function executeUtility() { 9 | process.argv = casetest.argv; 10 | await main(); 11 | } 12 | 13 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 14 | expect(await executeUtility()).not.toBe(null); 15 | }, 60000); 16 | -------------------------------------------------------------------------------- /test/TestCases/Case01/Case01.02.test.js: -------------------------------------------------------------------------------- 1 | import { readJSONFile, checkOutputFilesSize, checkOutputFilesContent, checkFolderContainsFiles } from '../../testLib'; 2 | 3 | const casetest = readJSONFile('./test/TestCases/Case01/case.json'); 4 | 5 | describe('Validate output files', () => { 6 | const expectedFiles = [ 7 | 'output.resolver.graphql.js', 8 | 'output.jsresolver.graphql.js', 9 | 'output.resolver.schema.json', 10 | 'output.schema.graphql', 11 | 'output.source.schema.graphql' 12 | ]; 13 | checkFolderContainsFiles('./test/TestCases/Case01/output', expectedFiles); 14 | checkOutputFilesSize('./test/TestCases/Case01/output', casetest.testOutputFilesSize, './test/TestCases/Case01/outputReference'); 15 | checkOutputFilesContent('./test/TestCases/Case01/output', casetest.testOutputFilesContent, './test/TestCases/Case01/outputReference'); 16 | }); -------------------------------------------------------------------------------- /test/TestCases/Case01/Case01.03.test.js: -------------------------------------------------------------------------------- 1 | import { readJSONFile, testResolverQueriesResults } from '../../testLib'; 2 | import fs from "fs"; 3 | 4 | const casetest = readJSONFile('./test/TestCases/Case01/case.json'); 5 | 6 | try { 7 | await testResolverQueriesResults( './TestCases/Case01/output/output.resolver.graphql.js', 8 | './test/TestCases/Case01/queries', 9 | './test/TestCases/Case01/output/output.resolver.schema.json', 10 | casetest.host, 11 | casetest.port); 12 | } finally { 13 | fs.rmSync('./test/TestCases/Case01/output', {recursive: true}); 14 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/case.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unit Test (Air Routes)", 3 | "description":"Start from graphql schema with directives, using Air Routes unit test schema", 4 | "argv":["--quiet", 5 | "--input-schema-file", "./test/TestCases/airports.source.schema.graphql", 6 | "--input-schema-changes-file", "./test/TestCases/Case01/input/changesAirport.json", 7 | "--output-js-resolver-file", "./test/TestCases/Case01/output/output.jsresolver.graphql.js", 8 | "--output-folder-path", "./test/TestCases/Case01/output", 9 | "--output-no-lambda-zip"], 10 | "host": "", 11 | "port": "", 12 | "testOutputFilesSize": ["output.schema.graphql", "output.source.schema.graphql"], 13 | "testOutputFilesContent": ["output.schema.graphql", "output.source.schema.graphql"] 14 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/input/changesAirport.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "type": "Airport", "field": "outboundRoutesCountAdd", "action": "add", "value":"outboundRoutesCountAdd: Int @graphQuery(statement: \"MATCH (this)-[r:route]->(a) RETURN count(r)\")"}, 3 | { "type": "Mutation", "field": "deleteNodeVersion", "action": "remove", "value": "" }, 4 | { "type": "Mutation", "field": "createNodeVersion", "action": "remove", "value": "" }, 5 | { "type": "Query", "field": "getAirportWithGremlin", "action": "add", "value":"getAirportWithGremlin(code:String): Airport @graphQuery(statement: \"g.V().has('airport', 'code', '$code').elementMap()\")"}, 6 | { "type": "Query", "field": "getCountriesCount", "action": "add", "value":"getCountriesCount: Int @graphQuery(statement: \"g.V().hasLabel('country').count()\")"} 7 | ] -------------------------------------------------------------------------------- /test/TestCases/Case01/outputReference/output.schema.graphql: -------------------------------------------------------------------------------- 1 | type Continent { 2 | id: ID! 3 | code: String 4 | type: String 5 | desc: String 6 | airportContainssOut(filter: AirportInput, options: Options): [Airport] 7 | contains: Contains 8 | } 9 | 10 | input ContinentInput { 11 | id: ID 12 | code: String 13 | type: String 14 | desc: String 15 | } 16 | 17 | type Country { 18 | _id: ID! 19 | code: String 20 | type: String 21 | desc: String 22 | airportContainssOut(filter: AirportInput, options: Options): [Airport] 23 | contains: Contains 24 | } 25 | 26 | input CountryInput { 27 | _id: ID 28 | code: String 29 | type: String 30 | desc: String 31 | } 32 | 33 | type Version { 34 | _id: ID! 35 | date: String 36 | code: String 37 | author: String 38 | type: String 39 | desc: String 40 | } 41 | 42 | input VersionInput { 43 | _id: ID 44 | date: String 45 | code: String 46 | author: String 47 | type: String 48 | desc: String 49 | } 50 | 51 | type Airport { 52 | _id: ID! 53 | country: String 54 | longest: Int 55 | code: String 56 | city: String 57 | elev: Int 58 | icao: String 59 | lon: Float 60 | runways: Int 61 | region: String 62 | type: String 63 | lat: Float 64 | desc2: String 65 | outboundRoutesCount: Int 66 | continentContainsIn: Continent 67 | countryContainsIn: Country 68 | airportRoutesOut(filter: AirportInput, options: Options): [Airport] 69 | airportRoutesIn(filter: AirportInput, options: Options): [Airport] 70 | contains: Contains 71 | route: Route 72 | outboundRoutesCountAdd: Int 73 | } 74 | 75 | input AirportInput { 76 | _id: ID 77 | country: String 78 | longest: Int 79 | code: String 80 | city: String 81 | elev: Int 82 | icao: String 83 | lon: Float 84 | runways: Int 85 | region: String 86 | type: String 87 | lat: Float 88 | desc: String 89 | } 90 | 91 | type Contains { 92 | _id: ID! 93 | } 94 | 95 | type Route { 96 | _id: ID! 97 | dist: Int 98 | } 99 | 100 | input RouteInput { 101 | dist: Int 102 | } 103 | 104 | input Options { 105 | limit: Int 106 | } 107 | 108 | type Query { 109 | getAirport(code: String): Airport 110 | getAirportConnection(fromCode: String!, toCode: String!): Airport 111 | getAirportWithGremlin(code: String): Airport 112 | getContinentsWithGremlin: [Continent] 113 | getCountriesCountGremlin: Int 114 | getNodeContinent(filter: ContinentInput): Continent 115 | getNodeContinents(filter: ContinentInput, options: Options): [Continent] 116 | getNodeCountry(filter: CountryInput): Country 117 | getNodeCountrys(filter: CountryInput, options: Options): [Country] 118 | getNodeVersion(filter: VersionInput): Version 119 | getNodeVersions(filter: VersionInput, options: Options): [Version] 120 | getNodeAirport(filter: AirportInput): Airport 121 | getNodeAirports(filter: AirportInput, options: Options): [Airport] 122 | getAirportWithGremlin(code: String): Airport 123 | getCountriesCount: Int 124 | } 125 | 126 | type Mutation { 127 | createAirport(input: AirportInput!): Airport 128 | addRoute(fromAirportCode: String, toAirportCode: String, dist: Int): Route 129 | deleteAirport(id: ID): Int 130 | createNodeContinent(input: ContinentInput!): Continent 131 | updateNodeContinent(input: ContinentInput!): Continent 132 | deleteNodeContinent(_id: ID!): Boolean 133 | createNodeCountry(input: CountryInput!): Country 134 | updateNodeCountry(input: CountryInput!): Country 135 | deleteNodeCountry(_id: ID!): Boolean 136 | updateNodeVersion(input: VersionInput!): Version 137 | createNodeAirport(input: AirportInput!): Airport 138 | updateNodeAirport(input: AirportInput!): Airport 139 | deleteNodeAirport(_id: ID!): Boolean 140 | connectNodeContinentToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 141 | deleteEdgeContainsFromContinentToAirport(from_id: ID!, to_id: ID!): Boolean 142 | connectNodeCountryToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 143 | deleteEdgeContainsFromCountryToAirport(from_id: ID!, to_id: ID!): Boolean 144 | connectNodeAirportToNodeAirportEdgeRoute(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 145 | updateEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 146 | deleteEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!): Boolean 147 | } 148 | 149 | schema { 150 | query: Query 151 | mutation: Mutation 152 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/outputReference/output.source.schema.graphql: -------------------------------------------------------------------------------- 1 | type Continent @alias(property: "continent") { 2 | id: ID! @id 3 | code: String 4 | type: String 5 | desc: String 6 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) 7 | contains: Contains 8 | } 9 | 10 | input ContinentInput { 11 | id: ID @id 12 | code: String 13 | type: String 14 | desc: String 15 | } 16 | 17 | type Country @alias(property: "country") { 18 | _id: ID! @id 19 | code: String 20 | type: String 21 | desc: String 22 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) 23 | contains: Contains 24 | } 25 | 26 | input CountryInput { 27 | _id: ID @id 28 | code: String 29 | type: String 30 | desc: String 31 | } 32 | 33 | type Version @alias(property: "version") { 34 | _id: ID! @id 35 | date: String 36 | code: String 37 | author: String 38 | type: String 39 | desc: String 40 | } 41 | 42 | input VersionInput { 43 | _id: ID @id 44 | date: String 45 | code: String 46 | author: String 47 | type: String 48 | desc: String 49 | } 50 | 51 | type Airport @alias(property: "airport") { 52 | _id: ID! @id 53 | country: String 54 | longest: Int 55 | code: String 56 | city: String 57 | elev: Int 58 | icao: String 59 | lon: Float 60 | runways: Int 61 | region: String 62 | type: String 63 | lat: Float 64 | desc2: String @alias(property: "desc") 65 | outboundRoutesCount: Int @graphQuery(statement: "MATCH (this)-[r:route]->(a) RETURN count(r)") 66 | continentContainsIn: Continent @relationship(edgeType: "contains", direction: IN) 67 | countryContainsIn: Country @relationship(edgeType: "contains", direction: IN) 68 | airportRoutesOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: OUT) 69 | airportRoutesIn(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: IN) 70 | contains: Contains 71 | route: Route 72 | outboundRoutesCountAdd: Int @graphQuery(statement: "MATCH (this)-[r:route]->(a) RETURN count(r)") 73 | } 74 | 75 | input AirportInput { 76 | _id: ID @id 77 | country: String 78 | longest: Int 79 | code: String 80 | city: String 81 | elev: Int 82 | icao: String 83 | lon: Float 84 | runways: Int 85 | region: String 86 | type: String 87 | lat: Float 88 | desc: String 89 | } 90 | 91 | type Contains @alias(property: "contains") { 92 | _id: ID! @id 93 | } 94 | 95 | type Route @alias(property: "route") { 96 | _id: ID! @id 97 | dist: Int 98 | } 99 | 100 | input RouteInput { 101 | dist: Int 102 | } 103 | 104 | input Options { 105 | limit: Int 106 | } 107 | 108 | type Query { 109 | getAirport(code: String): Airport 110 | getAirportConnection(fromCode: String!, toCode: String!): Airport @cypher(statement: "MATCH (:airport{code: '$fromCode'})-[:route]->(this:airport)-[:route]->(:airport{code:'$toCode'})") 111 | getAirportWithGremlin(code: String): Airport @graphQuery(statement: "g.V().has('airport', 'code', '$code').elementMap()") 112 | getContinentsWithGremlin: [Continent] @graphQuery(statement: "g.V().hasLabel('continent').elementMap().fold()") 113 | getCountriesCountGremlin: Int @graphQuery(statement: "g.V().hasLabel('country').count()") 114 | getNodeContinent(filter: ContinentInput): Continent 115 | getNodeContinents(filter: ContinentInput, options: Options): [Continent] 116 | getNodeCountry(filter: CountryInput): Country 117 | getNodeCountrys(filter: CountryInput, options: Options): [Country] 118 | getNodeVersion(filter: VersionInput): Version 119 | getNodeVersions(filter: VersionInput, options: Options): [Version] 120 | getNodeAirport(filter: AirportInput): Airport 121 | getNodeAirports(filter: AirportInput, options: Options): [Airport] 122 | getAirportWithGremlin(code: String): Airport @graphQuery(statement: "g.V().has('airport', 'code', '$code').elementMap()") 123 | getCountriesCount: Int @graphQuery(statement: "g.V().hasLabel('country').count()") 124 | } 125 | 126 | type Mutation { 127 | createAirport(input: AirportInput!): Airport @graphQuery(statement: "CREATE (this:airport {$input}) RETURN this") 128 | 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") 129 | deleteAirport(id: ID): Int @graphQuery(statement: "MATCH (this:airport) WHERE ID(this) = '$id' DETACH DELETE this") 130 | createNodeContinent(input: ContinentInput!): Continent 131 | updateNodeContinent(input: ContinentInput!): Continent 132 | deleteNodeContinent(_id: ID!): Boolean 133 | createNodeCountry(input: CountryInput!): Country 134 | updateNodeCountry(input: CountryInput!): Country 135 | deleteNodeCountry(_id: ID!): Boolean 136 | updateNodeVersion(input: VersionInput!): Version 137 | createNodeAirport(input: AirportInput!): Airport 138 | updateNodeAirport(input: AirportInput!): Airport 139 | deleteNodeAirport(_id: ID!): Boolean 140 | connectNodeContinentToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 141 | deleteEdgeContainsFromContinentToAirport(from_id: ID!, to_id: ID!): Boolean 142 | connectNodeCountryToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 143 | deleteEdgeContainsFromCountryToAirport(from_id: ID!, to_id: ID!): Boolean 144 | connectNodeAirportToNodeAirportEdgeRoute(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 145 | updateEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 146 | deleteEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!): Boolean 147 | } 148 | 149 | schema { 150 | query: Query 151 | mutation: Mutation 152 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0000.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "description": "", 4 | "graphql": "", 5 | "result":{} 6 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0001.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getAirport", 3 | "description": "Inference query from return type", 4 | "graphql": "query MyQuery {\n getAirport(code: \"SEA\") {\n city \n }\n}", 5 | "result":{ 6 | "city": "Seattle" 7 | } 8 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0002.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "getAirport _id", 3 | "description": "Get neptune _id", 4 | "graphql": "query MyQuery {\n getAirport(code: \"SEA\") {\n _id\n }\n }", 5 | "result":{ 6 | "_id": "22" 7 | } 8 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0003.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "3", 3 | "name": "getAirport nested type", 4 | "description": "Nested types single and array, references in and out", 5 | "graphql": "query MyQuery {\n getAirport(code: \"YKM\") {\n city\n continentContainsIn {\n desc\n }\n countryContainsIn {\n desc\n }\n airportRoutesOut {\n code\n }\n }\n }", 6 | "result":{ 7 | "airportRoutesOut": [ 8 | { 9 | "code": "SEA" 10 | } 11 | ], 12 | "city": "Yakima", 13 | "countryContainsIn": { 14 | "desc": "United States" 15 | }, 16 | "continentContainsIn": { 17 | "desc": "North America" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0005.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Type graph query 1", 3 | "description": "Type with graph query returning a scalar", 4 | "graphql": "query MyQuery {\n getAirport(code: \"SEA\") {\n outboundRoutesCount\n }\n }\n", 5 | "result": { 6 | "outboundRoutesCount": 122 7 | } 8 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0006.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Field alias", 3 | "description": "Map type name to different graph db property name", 4 | "graphql": "query MyQuery {\n getAirport(code: \"SEA\") {\n desc2\n }\n }\n", 5 | "result":{ 6 | "desc2": "Seattle-Tacoma" 7 | } 8 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0007.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphQuery type", 3 | "description": "Query using a graphQuery returing a type", 4 | "graphql": "query MyQuery {\n getAirportConnection(fromCode: \"SEA\", country: \"US\", toCode: \"BLQ\") {\n city\n code\n }\n }\n", 5 | "result":{ 6 | "code": "PHL", 7 | "city": "Philadelphia" 8 | } 9 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0008.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphQuery Gremlin type", 3 | "description": "Query using Gremlin returning a type", 4 | "graphql": "query MyQuery {\n getAirportWithGremlin(code: \"SEA\") {\n _id\n city\n runways\n }\n }\n", 5 | "result": { 6 | "_id": "22", 7 | "type": "airport", 8 | "code": "SEA", 9 | "desc2": "Seattle-Tacoma", 10 | "country": "US", 11 | "longest": 11901, 12 | "city": "Seattle", 13 | "lon": -122.30899810791, 14 | "elev": 432, 15 | "icao": "KSEA", 16 | "region": "US-WA", 17 | "runways": 3, 18 | "lat": 47.4490013122559 19 | } 20 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0009.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphQuery Gremlin type array", 3 | "description": "Query using Gremlin returning a type array", 4 | "graphql": "query MyQuery {\n getContinentsWithGremlin {\n code\n }\n }\n", 5 | "result": [ 6 | { 7 | "id": "3741", 8 | "type": "continent", 9 | "code": "EU", 10 | "desc": "Europe" 11 | }, 12 | { 13 | "id": "3742", 14 | "type": "continent", 15 | "code": "AF", 16 | "desc": "Africa" 17 | }, 18 | { 19 | "id": "3743", 20 | "type": "continent", 21 | "code": "NA", 22 | "desc": "North America" 23 | }, 24 | { 25 | "id": "3744", 26 | "type": "continent", 27 | "code": "SA", 28 | "desc": "South America" 29 | }, 30 | { 31 | "id": "3745", 32 | "type": "continent", 33 | "code": "AS", 34 | "desc": "Asia" 35 | }, 36 | { 37 | "id": "3746", 38 | "type": "continent", 39 | "code": "OC", 40 | "desc": "Oceania" 41 | }, 42 | { 43 | "id": "3747", 44 | "type": "continent", 45 | "code": "AN", 46 | "desc": "Antarctica" 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0010.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphQuery Gremlin scalar", 3 | "description": "Query using Gremlin returning a scalar", 4 | "graphql": "query MyQuery {\n getCountriesCountGremlin\n }\n", 5 | "result":237 6 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0011.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filter", 3 | "description": "", 4 | "graphql": "query MyQuery {\n getNodeAirport(filter: {code: \"SEA\"}) {\n city \n }\n}", 5 | "result":{ 6 | "city": "Seattle" 7 | } 8 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0012.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Limit in nested edge", 3 | "description": "", 4 | "graphql": "query MyQuery {\n getNodeAirport(filter: {code: \"SEA\"}) {\n airportRoutesOut(filter: {country: \"DE\"} options: {limit: 3}) {\n code\n }\n }\n }", 5 | "result":{ 6 | "airportRoutesOut": [ 7 | { 8 | "code": "CGN" 9 | }, 10 | { 11 | "code": "FRA" 12 | }, 13 | { 14 | "code": "MUC" 15 | } 16 | ] 17 | } 18 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0013.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Filter in nested edge", 3 | "description": "", 4 | "graphql": "query MyQuery {\n getNodeAirport(filter: {code: \"SEA\"}) {\n airportRoutesOut(filter: {code: \"LAX\"}) {\n city\n }\n city\n }\n }", 5 | "result":{ 6 | "airportRoutesOut": [ 7 | { 8 | "city": "Los Angeles" 9 | } 10 | ], 11 | "city": "Seattle" 12 | } 13 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0014.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Field graphQuery outboundRoutesCount", 3 | "description": "", 4 | "graphql": "query MyQuery {\n getNodeAirport(filter: {code: \"SEA\"}) {\n outboundRoutesCount\n }\n }", 5 | "result":{ 6 | "outboundRoutesCount": 122 7 | } 8 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0015.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mutation: create node", 3 | "description": "", 4 | "graphql": "mutation MyMutation {\n createNodeAirport(input: {code: \"NAX\", city: \"Reggio Emilia\"}) {\n code\n }\n }", 5 | "result":{ 6 | "code": "NAX" 7 | } 8 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0016.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mutation: update node", 3 | "description": "", 4 | "graphql": "mutation MyMutation {\n updateNodeAirport(input: {_id: \"22\", city: \"Seattle\"}) {\n city\n }\n }", 5 | "result":{ 6 | "city": "Seattle" 7 | } 8 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0017.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Get by _id", 3 | "description": "", 4 | "graphql": "query MyQuery {\n getNodeAirport(filter: {_id: \"22\"}) {\n city\n }\n }", 5 | "result":{ 6 | "city": "Seattle" 7 | } 8 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0018.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Limit option", 3 | "description": "", 4 | "graphql": "query MyQuery {\n getNodeAirports(options: {limit: 1}, filter: {code: \"SEA\"}) {\n city }\n }", 5 | "result":[ 6 | { 7 | "city": "Seattle" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0019.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Get different type of fields", 3 | "description": "", 4 | "graphql": "query MyQuery {\n getNodeAirport(filter: {code: \"SEA\"}) {\n _id\n city\n elev\n runways\n lat\n lon\n }\n }", 5 | "result":{ 6 | "city": "Seattle", 7 | "elev": 432, 8 | "lon": -122.30899810791, 9 | "_id": "22", 10 | "runways": 3, 11 | "lat": 47.4490013122559 12 | } 13 | } -------------------------------------------------------------------------------- /test/TestCases/Case01/queries/Query0020.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Filter by parameter with numeric value and return mix of numeric value types", 3 | "description": "", 4 | "graphql": "query MyQuery {\n getNodeAirports(filter: { city: \"Seattle\", runways: 3 }) {\n code\n lat\n lon\n elev\n}\n }", 5 | "result": [ 6 | { 7 | "code": "SEA", 8 | "elev": 432, 9 | "lon": -122.30899810791, 10 | "lat": 47.4490013122559 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /test/TestCases/Case02/Case02.01.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { readJSONFile } from '../../testLib'; 3 | import { main } from "../../../src/main"; 4 | import fs from "fs"; 5 | 6 | const casetest = readJSONFile('./test/TestCases/Case02/case.json'); 7 | 8 | async function executeUtility() { 9 | process.argv = casetest.argv; 10 | await main(); 11 | } 12 | 13 | describe('Validate successful execution', () => { 14 | afterAll(async () => { 15 | fs.rmSync('./test/TestCases/Case02/output', {recursive: true}); 16 | }); 17 | 18 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 19 | expect(await executeUtility()).not.toBe(null); 20 | }, 600000); 21 | }); 22 | -------------------------------------------------------------------------------- /test/TestCases/Case02/case.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Air Routes db json", 3 | "description":"Start from graph db json, from Air Routes db", 4 | "argv":[ "--quiet", 5 | "--input-graphdb-schema-file", "./test/TestCases/Case02/input/airports.graphdb.json", 6 | "--output-js-resolver-file", "./test/TestCases/Case02/output/output.jsresolver.graphql.js", 7 | "--output-folder-path", "./test/TestCases/Case02/output", 8 | "--output-no-lambda-zip"], 9 | "host": "", 10 | "port": "", 11 | "testOutputFilesSize": ["output.lambda.zip", "output.resolver.graphql.js", "output.schema.graphql", "output.source.schema.graphql"], 12 | "testOutputFilesContent": ["output.schema.graphql", "output.source.schema.graphql"] 13 | } -------------------------------------------------------------------------------- /test/TestCases/Case02/input/airports.graphdb.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "continent", 5 | "properties": [ 6 | { "name": "code", "type": "String" }, 7 | { "name": "type", "type": "String" }, 8 | { "name": "desc", "type": "String" } 9 | ] 10 | }, 11 | { 12 | "label": "country", 13 | "properties": [ 14 | { "name": "code", "type": "String" }, 15 | { "name": "type", "type": "String" }, 16 | { "name": "desc", "type": "String" } 17 | ] 18 | }, 19 | { 20 | "label": "version", 21 | "properties": [ 22 | { "name": "date", "type": "String" }, 23 | { "name": "code", "type": "String" }, 24 | { "name": "author", "type": "String" }, 25 | { "name": "type", "type": "String" }, 26 | { "name": "desc", "type": "String" } 27 | ] 28 | }, 29 | { 30 | "label": "airport", 31 | "properties": [ 32 | { "name": "country", "type": "String" }, 33 | { "name": "longest", "type": "Int" }, 34 | { "name": "code", "type": "String" }, 35 | { "name": "city", "type": "String" }, 36 | { "name": "elev", "type": "Int" }, 37 | { "name": "icao", "type": "String" }, 38 | { "name": "lon", "type": "Float" }, 39 | { "name": "runways", "type": "Int" }, 40 | { "name": "region", "type": "String" }, 41 | { "name": "type", "type": "String" }, 42 | { "name": "lat", "type": "Float" }, 43 | { "name": "desc", "type": "String" } 44 | ] 45 | } 46 | ], 47 | "edgeStructures": [ 48 | { 49 | "label": "contains", 50 | "properties": [], 51 | "directions": [ 52 | { "from": "continent", "to": "airport", "relationship": "ONE-MANY" }, 53 | { "from": "country", "to": "airport", "relationship": "ONE-MANY" } 54 | ] 55 | }, 56 | { 57 | "label": "route", 58 | "properties": [ 59 | { "name": "dist", "type": "Int" } 60 | ], 61 | "directions": [ 62 | { "from": "airport", "to": "airport", "relationship": "MANY-MANY" } 63 | ] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /test/TestCases/Case02/queries/Query0000.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "description": "", 4 | "graphql": "", 5 | "resolved": "", 6 | "result":"" 7 | } -------------------------------------------------------------------------------- /test/TestCases/Case03/Case03.01.test.js: -------------------------------------------------------------------------------- 1 | import { readJSONFile } from '../../testLib'; 2 | import { main } from "../../../src/main"; 3 | import fs from "fs"; 4 | 5 | const casetest = readJSONFile('./test/TestCases/Case03/case.json'); 6 | 7 | async function executeUtility() { 8 | process.argv = casetest.argv; 9 | await main(); 10 | } 11 | 12 | describe('Validate successful execution', () => { 13 | afterAll(async () => { 14 | fs.rmSync('./test/TestCases/Case03/output', {recursive: true}); 15 | }); 16 | 17 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 18 | expect(await executeUtility()).not.toBe(null); 19 | }, 600000); 20 | }); 21 | -------------------------------------------------------------------------------- /test/TestCases/Case03/case.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Todo Schema", 3 | "description":"", 4 | "argv":[ "--quiet", 5 | "--input-schema-file", "./test/TestCases/Case03/input/todo.schema.graphql", 6 | "--output-folder-path", "./test/TestCases/Case03/output", 7 | "--output-no-lambda-zip"], 8 | "host": "", 9 | "port": "", 10 | "testOutputFilesSize": ["output.lambda.zip", "output.resolver.graphql.js", "output.schema.graphql", "output.source.schema.graphql"], 11 | "testOutputFilesContent": ["output.schema.graphql", "output.source.schema.graphql"] 12 | } -------------------------------------------------------------------------------- /test/TestCases/Case03/input/todo.schema.graphql: -------------------------------------------------------------------------------- 1 | 2 | type Todo { 3 | name: String 4 | description: String 5 | priority: Int 6 | status: String 7 | comments: [Comment] 8 | bestComment: Comment 9 | } 10 | 11 | type Comment { 12 | id: ID 13 | content: String 14 | } 15 | 16 | input Options { 17 | limit: Int 18 | } -------------------------------------------------------------------------------- /test/TestCases/Case04/Case04.01.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { jest } from '@jest/globals'; 3 | import { readJSONFile } from '../../testLib'; 4 | import { main } from "../../../src/main"; 5 | 6 | const casetest = readJSONFile('./test/TestCases/Case04/case.json'); 7 | 8 | async function executeUtility() { 9 | process.argv = casetest.argv; 10 | await main(); 11 | } 12 | 13 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 14 | expect(await executeUtility()).not.toBe(null); 15 | }, 30000); 16 | -------------------------------------------------------------------------------- /test/TestCases/Case04/Case04.02.test.js: -------------------------------------------------------------------------------- 1 | import { readJSONFile, checkOutputFileContent, checkOutputFilesSize } from '../../testLib'; 2 | import { sortNeptuneSchema } from './util'; 3 | import fs from "fs"; 4 | 5 | const casetest = readJSONFile('./test/TestCases/Case04/case.json'); 6 | 7 | checkOutputFilesSize('./test/TestCases/Case04/output', casetest.testOutputFilesSize, './test/TestCases/Case04/outputReference'); 8 | 9 | const neptuneSchema = readJSONFile('./test/TestCases/Case04/output/output.neptune.schema.json'); 10 | const refNeptuneSchema = readJSONFile('./test/TestCases/Case04/outputReference/output.neptune.schema.json'); 11 | 12 | describe('Validate neptune schema', () => { 13 | afterAll(async () => { 14 | fs.rmSync('./test/TestCases/Case04/output', {recursive: true}); 15 | }); 16 | 17 | // note that this test can be flaky depending on how the air routes sample data was loaded into neptune 18 | // for more consistent results, use neptune notebook %seed with gremlin language 19 | checkOutputFileContent( 20 | 'output.neptune.schema.json', 21 | sortNeptuneSchema(neptuneSchema), 22 | sortNeptuneSchema(refNeptuneSchema), 23 | { checkRefIntegrity: false } 24 | ); 25 | }); 26 | -------------------------------------------------------------------------------- /test/TestCases/Case04/case.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Get Database Schema", 3 | "description":"", 4 | "argv":["--quiet", 5 | "--input-graphdb-schema-neptune-endpoint", ":", 6 | "--output-folder-path", "./test/TestCases/Case04/output", 7 | "--output-no-lambda-zip"], 8 | "host": "", 9 | "port": "", 10 | "testOutputFilesSize": ["output.neptune.schema.json"] 11 | } -------------------------------------------------------------------------------- /test/TestCases/Case04/outputReference/output.neptune.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodeStructures": [ 3 | { 4 | "label": "airport", 5 | "properties": [ 6 | { 7 | "name": "type", 8 | "type": "String" 9 | }, 10 | { 11 | "name": "city", 12 | "type": "String" 13 | }, 14 | { 15 | "name": "icao", 16 | "type": "String" 17 | }, 18 | { 19 | "name": "code", 20 | "type": "String" 21 | }, 22 | { 23 | "name": "country", 24 | "type": "String" 25 | }, 26 | { 27 | "name": "lat", 28 | "type": "Float" 29 | }, 30 | { 31 | "name": "longest", 32 | "type": "Int" 33 | }, 34 | { 35 | "name": "runways", 36 | "type": "Int" 37 | }, 38 | { 39 | "name": "desc", 40 | "type": "String" 41 | }, 42 | { 43 | "name": "lon", 44 | "type": "Float" 45 | }, 46 | { 47 | "name": "region", 48 | "type": "String" 49 | }, 50 | { 51 | "name": "elev", 52 | "type": "Int" 53 | } 54 | ] 55 | }, 56 | { 57 | "label": "version", 58 | "properties": [ 59 | { 60 | "name": "date", 61 | "type": "String" 62 | }, 63 | { 64 | "name": "desc", 65 | "type": "String" 66 | }, 67 | { 68 | "name": "author", 69 | "type": "String" 70 | }, 71 | { 72 | "name": "type", 73 | "type": "String" 74 | }, 75 | { 76 | "name": "code", 77 | "type": "String" 78 | } 79 | ] 80 | }, 81 | { 82 | "label": "country", 83 | "properties": [ 84 | { 85 | "name": "type", 86 | "type": "String" 87 | }, 88 | { 89 | "name": "code", 90 | "type": "String" 91 | }, 92 | { 93 | "name": "desc", 94 | "type": "String" 95 | } 96 | ] 97 | }, 98 | { 99 | "label": "continent", 100 | "properties": [ 101 | { 102 | "name": "type", 103 | "type": "String" 104 | }, 105 | { 106 | "name": "code", 107 | "type": "String" 108 | }, 109 | { 110 | "name": "desc", 111 | "type": "String" 112 | } 113 | ] 114 | } 115 | ], 116 | "edgeStructures": [ 117 | { 118 | "label": "route", 119 | "directions": [ 120 | { 121 | "from": "airport", 122 | "to": "airport", 123 | "relationship": "MANY-MANY" 124 | } 125 | ], 126 | "properties": [ 127 | { 128 | "name": "dist", 129 | "type": "Int" 130 | } 131 | ] 132 | }, 133 | { 134 | "label": "contains", 135 | "directions": [ 136 | { 137 | "from": "continent", 138 | "to": "airport", 139 | "relationship": "ONE-MANY" 140 | }, 141 | { 142 | "from": "country", 143 | "to": "airport", 144 | "relationship": "ONE-MANY" 145 | } 146 | ], 147 | "properties": [] 148 | } 149 | ] 150 | } -------------------------------------------------------------------------------- /test/TestCases/Case04/util.js: -------------------------------------------------------------------------------- 1 | export function sortNeptuneSchema(schema) { 2 | const { nodeStructures, edgeStructures } = schema; 3 | 4 | nodeStructures.forEach( 5 | structure => structure.properties.sort((a, b) => a.name.localeCompare(b.name)) 6 | ); 7 | edgeStructures.forEach( 8 | structure => structure.directions.sort( 9 | (a, b) => a.from.localeCompare(b.from) || a.to.localeCompare(b.to) 10 | ) 11 | ); 12 | 13 | nodeStructures.sort((a, b) => a.label.localeCompare(b.label)); 14 | edgeStructures.sort((a, b) => a.label.localeCompare(b.label)); 15 | 16 | return schema; 17 | } 18 | -------------------------------------------------------------------------------- /test/TestCases/Case05/Case05.01.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { jest } from '@jest/globals'; 3 | import { readJSONFile } from '../../testLib'; 4 | import { main } from "../../../src/main"; 5 | 6 | const casetest = readJSONFile('./test/TestCases/Case05/case01.json'); 7 | 8 | async function executeUtility() { 9 | process.argv = casetest.argv; 10 | await main(); 11 | } 12 | 13 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 14 | expect(await executeUtility()).not.toBe(null); 15 | }, 600000); 16 | -------------------------------------------------------------------------------- /test/TestCases/Case05/Case05.02.test.js: -------------------------------------------------------------------------------- 1 | import { readJSONFile } from '../../testLib'; 2 | import { main } from "../../../src/main"; 3 | import fs from "fs"; 4 | 5 | const casetest = readJSONFile('./test/TestCases/Case05/case02.json'); 6 | 7 | async function executeUtility() { 8 | process.argv = casetest.argv; 9 | await main(); 10 | } 11 | 12 | describe('Cleanup resources', () => { 13 | afterAll(async () => { 14 | fs.rmSync('./test/TestCases/Case05/output', {recursive: true}); 15 | }); 16 | 17 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 18 | expect(await executeUtility()).not.toBe(null); 19 | }, 600000); 20 | }); 21 | -------------------------------------------------------------------------------- /test/TestCases/Case05/case01.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unit Test (Air Routes) Pipeline", 3 | "description":"Create pipeline", 4 | "argv":["--quiet", 5 | "--input-schema-file", "./test/TestCases/airports.source.schema.graphql", 6 | "--output-folder-path", "./test/TestCases/Case05/output", 7 | "--create-update-aws-pipeline", 8 | "--create-update-aws-pipeline-name", "AirportsJestTest", 9 | "--create-update-aws-pipeline-neptune-endpoint", ":", 10 | "--output-resolver-query-https"], 11 | "host": "", 12 | "port": "", 13 | "testOutputFilesSize": ["output.resolver.graphql.js", "output.schema.graphql", "output.source.schema.graphql"], 14 | "testOutputFilesContent": ["output.schema.graphql", "output.source.schema.graphql"] 15 | } -------------------------------------------------------------------------------- /test/TestCases/Case05/case02.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unit Test (Air Routes) Remove Pipeline", 3 | "description":"Remove pipeline", 4 | "argv":["--quiet", 5 | "--remove-aws-pipeline-name", "AirportsJestTest", 6 | "--output-folder-path", "./test/TestCases/Case05/output"], 7 | "host": "", 8 | "port": "", 9 | "testOutputFilesSize": ["output.resolver.graphql.js", "output.schema.graphql", "output.source.schema.graphql"], 10 | "testOutputFilesContent": ["output.schema.graphql", "output.source.schema.graphql"] 11 | } -------------------------------------------------------------------------------- /test/TestCases/Case06/Case06.01.test.js: -------------------------------------------------------------------------------- 1 | 2 | import { jest } from '@jest/globals'; 3 | import { readJSONFile } from '../../testLib'; 4 | import { main } from "../../../src/main"; 5 | 6 | const casetest = readJSONFile('./test/TestCases/Case06/case.json'); 7 | 8 | async function executeUtility() { 9 | process.argv = casetest.argv; 10 | await main(); 11 | } 12 | 13 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 14 | expect(await executeUtility()).not.toBe(null); 15 | }, 600000); 16 | -------------------------------------------------------------------------------- /test/TestCases/Case06/Case06.02.test.js: -------------------------------------------------------------------------------- 1 | import {checkFileContains, readJSONFile} from '../../testLib'; 2 | import fs from "fs"; 3 | 4 | const casetest = readJSONFile('./test/TestCases/Case06/case.json'); 5 | let neptuneType = 'neptune-db'; 6 | if (casetest.host.includes('neptune-graph')) { 7 | neptuneType = 'neptune-graph'; 8 | } 9 | describe('Validate file content', () => { 10 | afterAll(async () => { 11 | fs.rmSync('./test/TestCases/Case06/output', {recursive: true}); 12 | }); 13 | 14 | checkFileContains('./test/TestCases/Case06/output/AirportCDKTestJest-cdk.js', [ 15 | 'const NAME = \'AirportCDKTestJest\';', 16 | 'const NEPTUNE_HOST = \'' + casetest.host + '\';', 17 | 'const NEPTUNE_PORT = \'' + casetest.port + '\';', 18 | 'const NEPTUNE_TYPE = \'' + neptuneType + '\';', 19 | 'vpcSubnets', 20 | 'securityGroups' 21 | ]); 22 | }); 23 | -------------------------------------------------------------------------------- /test/TestCases/Case06/case.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unit Test (Air Routes) CDK Pipeline", 3 | "description":"Create CDK pipeline", 4 | "argv":["--quiet", 5 | "--input-schema-file", "./test/TestCases/airports.source.schema.graphql", 6 | "--output-folder-path", "./test/TestCases/Case06/output", 7 | "--output-aws-pipeline-cdk", 8 | "--output-aws-pipeline-cdk-name", "AirportCDKTestJest", 9 | "--output-aws-pipeline-cdk-neptune-database-name", "airport00", 10 | "--output-aws-pipeline-cdk-region", "us-east-1", 11 | "--output-aws-pipeline-cdk-neptune-endpoint", ":", 12 | "--output-resolver-query-https"], 13 | "host": "", 14 | "port": "", 15 | "testOutputFilesSize": ["output.resolver.graphql.js", "output.schema.graphql", "output.source.schema.graphql"], 16 | "testOutputFilesContent": ["output.schema.graphql", "output.source.schema.graphql"] 17 | } -------------------------------------------------------------------------------- /test/TestCases/Case07/Case07.01.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile} from '../../testLib'; 2 | import {main} from "../../../src/main"; 3 | 4 | const casetest = readJSONFile('./test/TestCases/Case07/case01.json'); 5 | 6 | async function executeUtility() { 7 | process.argv = casetest.argv; 8 | await main(); 9 | } 10 | 11 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 12 | expect(await executeUtility()).not.toBe(null); 13 | }, 600000); 14 | -------------------------------------------------------------------------------- /test/TestCases/Case07/Case07.02.test.js: -------------------------------------------------------------------------------- 1 | import {checkFolderContainsFiles, checkOutputFilesContent, checkOutputZipLambdaUsesSdk, readJSONFile} from '../../testLib'; 2 | 3 | describe('Validate output files', () => { 4 | const casetest = readJSONFile('./test/TestCases/Case07/case01.json'); 5 | const expectedFiles = [ 6 | 'AirportsJestSDKTest.resolver.schema.json', 7 | 'AirportsJestSDKTest.zip', 8 | 'AirportsJestSDKTest-resources.json', 9 | 'output.resolver.graphql.js', 10 | 'output.schema.graphql', 11 | 'output.source.schema.graphql' 12 | ]; 13 | checkFolderContainsFiles('./test/TestCases/Case07/output', expectedFiles); 14 | checkOutputFilesContent('./test/TestCases/Case07/output', casetest.testOutputFilesContent, './test/TestCases/Case07/outputReference'); 15 | checkOutputZipLambdaUsesSdk('./test/TestCases/Case07/output', './test/TestCases/Case07/output/AirportsJestSDKTest.zip'); 16 | }); -------------------------------------------------------------------------------- /test/TestCases/Case07/Case07.03.test.js: -------------------------------------------------------------------------------- 1 | import { readJSONFile } from '../../testLib'; 2 | import { main } from "../../../src/main"; 3 | import fs from "fs"; 4 | 5 | const casetest = readJSONFile('./test/TestCases/Case07/case02.json'); 6 | 7 | async function executeUtility() { 8 | process.argv = casetest.argv; 9 | await main(); 10 | } 11 | describe('Cleanup resources', () => { 12 | afterAll(async () => { 13 | fs.rmSync('./test/TestCases/Case07/output', {recursive: true}); 14 | }); 15 | 16 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 17 | expect(await executeUtility()).not.toBe(null); 18 | }, 600000); 19 | }); 20 | -------------------------------------------------------------------------------- /test/TestCases/Case07/case01.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unit Test (Air Routes) Pipeline", 3 | "description":"Create SDK pipeline", 4 | "argv":["--quiet", 5 | "--input-schema-file", "./test/TestCases/airports.source.schema.graphql", 6 | "--output-folder-path", "./test/TestCases/Case07/output", 7 | "--output-schema-file", "./test/TestCases/Case07/output/output.schema.graphql", 8 | "--output-source-schema-file", "./test/TestCases/Case07/output/output.source.schema.graphql", 9 | "--output-js-resolver-file", "./test/TestCases/Case07/output/output.resolver.graphql.js", 10 | "--create-update-aws-pipeline", 11 | "--create-update-aws-pipeline-name", "AirportsJestSDKTest", 12 | "--create-update-aws-pipeline-neptune-endpoint", ":", 13 | "--output-resolver-query-sdk"], 14 | "host": "", 15 | "port": "", 16 | "testOutputFilesContent": ["output.schema.graphql", "output.source.schema.graphql"] 17 | } -------------------------------------------------------------------------------- /test/TestCases/Case07/case02.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unit Test (Air Routes) Remove Pipeline", 3 | "description":"Remove SDK pipeline", 4 | "argv":["--quiet", 5 | "--remove-aws-pipeline-name", "AirportsJestSDKTest", 6 | "--output-folder-path", "./test/TestCases/Case07/output"], 7 | "host": "", 8 | "port": "" 9 | } -------------------------------------------------------------------------------- /test/TestCases/Case07/outputReference/output.schema.graphql: -------------------------------------------------------------------------------- 1 | type Continent { 2 | id: ID! 3 | code: String 4 | type: String 5 | desc: String 6 | airportContainssOut(filter: AirportInput, options: Options): [Airport] 7 | contains: Contains 8 | } 9 | 10 | input ContinentInput { 11 | id: ID 12 | code: String 13 | type: String 14 | desc: String 15 | } 16 | 17 | type Country { 18 | _id: ID! 19 | code: String 20 | type: String 21 | desc: String 22 | airportContainssOut(filter: AirportInput, options: Options): [Airport] 23 | contains: Contains 24 | } 25 | 26 | input CountryInput { 27 | _id: ID 28 | code: String 29 | type: String 30 | desc: String 31 | } 32 | 33 | type Version { 34 | _id: ID! 35 | date: String 36 | code: String 37 | author: String 38 | type: String 39 | desc: String 40 | } 41 | 42 | input VersionInput { 43 | _id: ID 44 | date: String 45 | code: String 46 | author: String 47 | type: String 48 | desc: String 49 | } 50 | 51 | type Airport { 52 | _id: ID! 53 | country: String 54 | longest: Int 55 | code: String 56 | city: String 57 | elev: Int 58 | icao: String 59 | lon: Float 60 | runways: Int 61 | region: String 62 | type: String 63 | lat: Float 64 | desc2: String 65 | outboundRoutesCount: Int 66 | continentContainsIn: Continent 67 | countryContainsIn: Country 68 | airportRoutesOut(filter: AirportInput, options: Options): [Airport] 69 | airportRoutesIn(filter: AirportInput, options: Options): [Airport] 70 | contains: Contains 71 | route: Route 72 | } 73 | 74 | input AirportInput { 75 | _id: ID 76 | country: String 77 | longest: Int 78 | code: String 79 | city: String 80 | elev: Int 81 | icao: String 82 | lon: Float 83 | runways: Int 84 | region: String 85 | type: String 86 | lat: Float 87 | desc: String 88 | } 89 | 90 | type Contains { 91 | _id: ID! 92 | } 93 | 94 | type Route { 95 | _id: ID! 96 | dist: Int 97 | } 98 | 99 | input RouteInput { 100 | dist: Int 101 | } 102 | 103 | input Options { 104 | limit: Int 105 | } 106 | 107 | type Query { 108 | getAirport(code: String): Airport 109 | getAirportConnection(fromCode: String!, toCode: String!): Airport 110 | getAirportWithGremlin(code: String): Airport 111 | getContinentsWithGremlin: [Continent] 112 | getCountriesCountGremlin: Int 113 | getNodeContinent(filter: ContinentInput): Continent 114 | getNodeContinents(filter: ContinentInput, options: Options): [Continent] 115 | getNodeCountry(filter: CountryInput): Country 116 | getNodeCountrys(filter: CountryInput, options: Options): [Country] 117 | getNodeVersion(filter: VersionInput): Version 118 | getNodeVersions(filter: VersionInput, options: Options): [Version] 119 | getNodeAirport(filter: AirportInput): Airport 120 | getNodeAirports(filter: AirportInput, options: Options): [Airport] 121 | } 122 | 123 | type Mutation { 124 | createAirport(input: AirportInput!): Airport 125 | addRoute(fromAirportCode: String, toAirportCode: String, dist: Int): Route 126 | deleteAirport(id: ID): Int 127 | createNodeContinent(input: ContinentInput!): Continent 128 | updateNodeContinent(input: ContinentInput!): Continent 129 | deleteNodeContinent(_id: ID!): Boolean 130 | createNodeCountry(input: CountryInput!): Country 131 | updateNodeCountry(input: CountryInput!): Country 132 | deleteNodeCountry(_id: ID!): Boolean 133 | createNodeVersion(input: VersionInput!): Version 134 | updateNodeVersion(input: VersionInput!): Version 135 | deleteNodeVersion(_id: ID!): Boolean 136 | createNodeAirport(input: AirportInput!): Airport 137 | updateNodeAirport(input: AirportInput!): Airport 138 | deleteNodeAirport(_id: ID!): Boolean 139 | connectNodeContinentToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 140 | deleteEdgeContainsFromContinentToAirport(from_id: ID!, to_id: ID!): Boolean 141 | connectNodeCountryToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 142 | deleteEdgeContainsFromCountryToAirport(from_id: ID!, to_id: ID!): Boolean 143 | connectNodeAirportToNodeAirportEdgeRoute(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 144 | updateEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 145 | deleteEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!): Boolean 146 | } 147 | 148 | schema { 149 | query: Query 150 | mutation: Mutation 151 | } -------------------------------------------------------------------------------- /test/TestCases/Case07/outputReference/output.source.schema.graphql: -------------------------------------------------------------------------------- 1 | type Continent @alias(property: "continent") { 2 | id: ID! @id 3 | code: String 4 | type: String 5 | desc: String 6 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) 7 | contains: Contains 8 | } 9 | 10 | input ContinentInput { 11 | id: ID @id 12 | code: String 13 | type: String 14 | desc: String 15 | } 16 | 17 | type Country @alias(property: "country") { 18 | _id: ID! @id 19 | code: String 20 | type: String 21 | desc: String 22 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) 23 | contains: Contains 24 | } 25 | 26 | input CountryInput { 27 | _id: ID @id 28 | code: String 29 | type: String 30 | desc: String 31 | } 32 | 33 | type Version @alias(property: "version") { 34 | _id: ID! @id 35 | date: String 36 | code: String 37 | author: String 38 | type: String 39 | desc: String 40 | } 41 | 42 | input VersionInput { 43 | _id: ID @id 44 | date: String 45 | code: String 46 | author: String 47 | type: String 48 | desc: String 49 | } 50 | 51 | type Airport @alias(property: "airport") { 52 | _id: ID! @id 53 | country: String 54 | longest: Int 55 | code: String 56 | city: String 57 | elev: Int 58 | icao: String 59 | lon: Float 60 | runways: Int 61 | region: String 62 | type: String 63 | lat: Float 64 | desc2: String @alias(property: "desc") 65 | outboundRoutesCount: Int @graphQuery(statement: "MATCH (this)-[r:route]->(a) RETURN count(r)") 66 | continentContainsIn: Continent @relationship(edgeType: "contains", direction: IN) 67 | countryContainsIn: Country @relationship(edgeType: "contains", direction: IN) 68 | airportRoutesOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: OUT) 69 | airportRoutesIn(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: IN) 70 | contains: Contains 71 | route: Route 72 | } 73 | 74 | input AirportInput { 75 | _id: ID @id 76 | country: String 77 | longest: Int 78 | code: String 79 | city: String 80 | elev: Int 81 | icao: String 82 | lon: Float 83 | runways: Int 84 | region: String 85 | type: String 86 | lat: Float 87 | desc: String 88 | } 89 | 90 | type Contains @alias(property: "contains") { 91 | _id: ID! @id 92 | } 93 | 94 | type Route @alias(property: "route") { 95 | _id: ID! @id 96 | dist: Int 97 | } 98 | 99 | input RouteInput { 100 | dist: Int 101 | } 102 | 103 | input Options { 104 | limit: Int 105 | } 106 | 107 | type Query { 108 | getAirport(code: String): Airport 109 | getAirportConnection(fromCode: String!, toCode: String!): Airport @cypher(statement: "MATCH (:airport{code: '$fromCode'})-[:route]->(this:airport)-[:route]->(:airport{code:'$toCode'})") 110 | getAirportWithGremlin(code: String): Airport @graphQuery(statement: "g.V().has('airport', 'code', '$code').elementMap()") 111 | getContinentsWithGremlin: [Continent] @graphQuery(statement: "g.V().hasLabel('continent').elementMap().fold()") 112 | getCountriesCountGremlin: Int @graphQuery(statement: "g.V().hasLabel('country').count()") 113 | getNodeContinent(filter: ContinentInput): Continent 114 | getNodeContinents(filter: ContinentInput, options: Options): [Continent] 115 | getNodeCountry(filter: CountryInput): Country 116 | getNodeCountrys(filter: CountryInput, options: Options): [Country] 117 | getNodeVersion(filter: VersionInput): Version 118 | getNodeVersions(filter: VersionInput, options: Options): [Version] 119 | getNodeAirport(filter: AirportInput): Airport 120 | getNodeAirports(filter: AirportInput, options: Options): [Airport] 121 | } 122 | 123 | type Mutation { 124 | createAirport(input: AirportInput!): Airport @graphQuery(statement: "CREATE (this:airport {$input}) RETURN this") 125 | 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") 126 | deleteAirport(id: ID): Int @graphQuery(statement: "MATCH (this:airport) WHERE ID(this) = '$id' DETACH DELETE this") 127 | createNodeContinent(input: ContinentInput!): Continent 128 | updateNodeContinent(input: ContinentInput!): Continent 129 | deleteNodeContinent(_id: ID!): Boolean 130 | createNodeCountry(input: CountryInput!): Country 131 | updateNodeCountry(input: CountryInput!): Country 132 | deleteNodeCountry(_id: ID!): Boolean 133 | createNodeVersion(input: VersionInput!): Version 134 | updateNodeVersion(input: VersionInput!): Version 135 | deleteNodeVersion(_id: ID!): Boolean 136 | createNodeAirport(input: AirportInput!): Airport 137 | updateNodeAirport(input: AirportInput!): Airport 138 | deleteNodeAirport(_id: ID!): Boolean 139 | connectNodeContinentToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 140 | deleteEdgeContainsFromContinentToAirport(from_id: ID!, to_id: ID!): Boolean 141 | connectNodeCountryToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 142 | deleteEdgeContainsFromCountryToAirport(from_id: ID!, to_id: ID!): Boolean 143 | connectNodeAirportToNodeAirportEdgeRoute(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 144 | updateEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 145 | deleteEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!): Boolean 146 | } 147 | 148 | schema { 149 | query: Query 150 | mutation: Mutation 151 | } -------------------------------------------------------------------------------- /test/TestCases/Case08/Case08.01.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile} from '../../testLib'; 2 | import {main} from "../../../src/main"; 3 | 4 | const casetest = readJSONFile('./test/TestCases/Case08/case01.json'); 5 | 6 | async function executeUtility() { 7 | process.argv = casetest.argv; 8 | await main(); 9 | } 10 | 11 | describe('Create Apollo Server output artifacts', () => { 12 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 13 | expect(await executeUtility()).not.toBe(null); 14 | }, 600000); 15 | }); -------------------------------------------------------------------------------- /test/TestCases/Case08/Case08.02.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile, testApolloArtifacts} from '../../testLib'; 2 | import fs from "fs"; 3 | import {parseNeptuneEndpoint} from "../../../src/util.js"; 4 | 5 | const testCase = readJSONFile('./test/TestCases/Case08/case01.json'); 6 | const testDbInfo = parseNeptuneEndpoint(testCase.host + ':' + testCase.port); 7 | const outputFolderPath = './test/TestCases/Case08/case08-01-output'; 8 | 9 | describe('Validate Apollo Server output artifacts', () => { 10 | afterAll(async () => { 11 | fs.rmSync(outputFolderPath, {recursive: true}); 12 | }); 13 | 14 | testApolloArtifacts(outputFolderPath, testDbInfo, false); 15 | }); -------------------------------------------------------------------------------- /test/TestCases/Case08/Case08.03.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile} from '../../testLib'; 2 | import {main} from "../../../src/main"; 3 | 4 | const casetest = readJSONFile('./test/TestCases/Case08/case02.json'); 5 | 6 | async function executeUtility() { 7 | process.argv = casetest.argv; 8 | await main(); 9 | } 10 | 11 | describe('Create Apollo Server output artifacts using customized output arguments', () => { 12 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 13 | expect(await executeUtility()).not.toBe(null); 14 | }, 600000); 15 | }); -------------------------------------------------------------------------------- /test/TestCases/Case08/Case08.04.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile, testApolloArtifacts} from '../../testLib'; 2 | import fs from "fs"; 3 | import {parseNeptuneEndpoint} from "../../../src/util.js"; 4 | 5 | const testCase = readJSONFile('./test/TestCases/Case08/case02.json'); 6 | const testDbInfo = parseNeptuneEndpoint(testCase.host + ':' + testCase.port); 7 | const outputFolderPath = './test/TestCases/Case08/case08-02-output'; 8 | 9 | describe('Validate Apollo Server output artifacts are created when using customized output arguments', () => { 10 | afterAll(async () => { 11 | fs.rmSync(outputFolderPath, {recursive: true}); 12 | }); 13 | 14 | testApolloArtifacts(outputFolderPath, testDbInfo, false); 15 | }); -------------------------------------------------------------------------------- /test/TestCases/Case08/Case08.05.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile} from '../../testLib'; 2 | import {main} from "../../../src/main"; 3 | 4 | const casetest = readJSONFile('./test/TestCases/Case08/case03.json'); 5 | 6 | async function executeUtility() { 7 | process.argv = casetest.argv; 8 | await main(); 9 | } 10 | 11 | describe('Create Apollo Server output artifacts from input schema file', () => { 12 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 13 | expect(await executeUtility()).not.toBe(null); 14 | }, 600000); 15 | }); -------------------------------------------------------------------------------- /test/TestCases/Case08/Case08.06.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile, testApolloArtifacts} from '../../testLib'; 2 | import fs from "fs"; 3 | import {parseNeptuneEndpoint} from "../../../src/util.js"; 4 | 5 | const testCase = readJSONFile('./test/TestCases/Case08/case03.json'); 6 | const testDbInfo = parseNeptuneEndpoint(testCase.host + ':' + testCase.port); 7 | const outputFolderPath = './test/TestCases/Case08/case08-03-output'; 8 | 9 | describe('Validate Apollo Server output artifacts are created when using an input schema file', () => { 10 | afterAll(async () => { 11 | fs.rmSync(outputFolderPath, {recursive: true}); 12 | }); 13 | 14 | testApolloArtifacts(outputFolderPath, testDbInfo, false); 15 | }); -------------------------------------------------------------------------------- /test/TestCases/Case08/case01.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Create Apollo Server ZIP", 3 | "description":"Test creation of Apollo Server ZIP", 4 | "argv":["--quiet", 5 | "--input-graphdb-schema-neptune-endpoint", ":", 6 | "--output-folder-path", "./test/TestCases/Case08/case08-01-output", 7 | "--create-update-apollo-server", 8 | "--output-resolver-query-https"], 9 | "host": "", 10 | "port": "" 11 | } -------------------------------------------------------------------------------- /test/TestCases/Case08/case02.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Create Apollo Server ZIP from customized output", 3 | "description":"Test creation of Apollo Server ZIP using customized output arguments", 4 | "argv":["--quiet", 5 | "--input-graphdb-schema-neptune-endpoint", ":", 6 | "--output-folder-path", "./test/TestCases/Case08/case08-02-output", 7 | "--create-update-apollo-server", 8 | "--output-resolver-query-https", 9 | "--output-lambda-resolver-file", "case08-02-resolver.js", 10 | "--output-schema-file", "case08-02.schema.graphql"], 11 | "host": "", 12 | "port": "" 13 | } -------------------------------------------------------------------------------- /test/TestCases/Case08/case03.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Create Apollo Server ZIP from input schema file", 3 | "description":"Test creation of Apollo Server ZIP using an input schema file", 4 | "argv":["--quiet", 5 | "--input-schema-file", "./test/TestCases/airports.source.schema.graphql", 6 | "--create-update-apollo-server-neptune-endpoint", ":", 7 | "--output-folder-path", "./test/TestCases/Case08/case08-03-output", 8 | "--create-update-apollo-server", 9 | "--output-resolver-query-https"], 10 | "host": "", 11 | "port": "" 12 | } 13 | -------------------------------------------------------------------------------- /test/TestCases/Case09/Case09.01.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile} from '../../testLib'; 2 | import {main} from "../../../src/main"; 3 | 4 | const casetest = readJSONFile('./test/TestCases/Case09/case01.json'); 5 | 6 | async function executeUtility() { 7 | process.argv = casetest.argv; 8 | await main(); 9 | } 10 | 11 | describe('Create Apollo Server Subgraph output artifacts', () => { 12 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 13 | expect(await executeUtility()).not.toBe(null); 14 | }, 600000); 15 | }); -------------------------------------------------------------------------------- /test/TestCases/Case09/Case09.02.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile, testApolloArtifacts} from '../../testLib'; 2 | import fs from "fs"; 3 | import {parseNeptuneEndpoint} from "../../../src/util.js"; 4 | 5 | const testCase = readJSONFile('./test/TestCases/Case09/case01.json'); 6 | const testDbInfo = parseNeptuneEndpoint(testCase.host + ':' + testCase.port); 7 | 8 | const outputFolderPath = './test/TestCases/Case09/output'; 9 | describe('Validate Apollo Server Subgraph output artifacts', () => { 10 | afterAll(async () => { 11 | fs.rmSync(outputFolderPath, {recursive: true}); 12 | }); 13 | 14 | testApolloArtifacts(outputFolderPath, testDbInfo, true); 15 | }); -------------------------------------------------------------------------------- /test/TestCases/Case09/Case09.03.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile} from '../../testLib'; 2 | import {main} from "../../../src/main"; 3 | 4 | const casetest = readJSONFile('./test/TestCases/Case09/case02.json'); 5 | 6 | async function executeUtility() { 7 | process.argv = casetest.argv; 8 | await main(); 9 | } 10 | 11 | describe('Create Apollo Server Subgraph output artifacts from input schema file', () => { 12 | test('Execute utility: ' + casetest.argv.join(' '), async () => { 13 | expect(await executeUtility()).not.toBe(null); 14 | }, 600000); 15 | }); -------------------------------------------------------------------------------- /test/TestCases/Case09/Case09.04.test.js: -------------------------------------------------------------------------------- 1 | import {readJSONFile, testApolloArtifacts} from '../../testLib'; 2 | import fs from "fs"; 3 | import {parseNeptuneEndpoint} from "../../../src/util.js"; 4 | 5 | const testCase = readJSONFile('./test/TestCases/Case09/case02.json'); 6 | const testDbInfo = parseNeptuneEndpoint(testCase.host + ':' + testCase.port); 7 | const outputFolderPath = './test/TestCases/Case09/case09-02-output'; 8 | 9 | describe('Validate Apollo Server Subgraph output artifacts are created when using an input schema file', () => { 10 | afterAll(async () => { 11 | fs.rmSync(outputFolderPath, {recursive: true}); 12 | }); 13 | 14 | testApolloArtifacts(outputFolderPath, testDbInfo, true); 15 | }); -------------------------------------------------------------------------------- /test/TestCases/Case09/case01.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Create Apollo Server ZIP", 3 | "description":"Test creation of Apollo Server Subgraph ZIP", 4 | "argv":["--quiet", 5 | "--input-graphdb-schema-neptune-endpoint", ":", 6 | "--output-folder-path", "./test/TestCases/Case09/output", 7 | "--create-update-apollo-server-subgraph", 8 | "--output-resolver-query-https"], 9 | "host": "", 10 | "port": "" 11 | } -------------------------------------------------------------------------------- /test/TestCases/Case09/case02.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Create Apollo Server Subgraph ZIP from input schema file", 3 | "description":"Test creation of Apollo Server Subgraph ZIP using an input schema file", 4 | "argv":["--quiet", 5 | "--input-schema-file", "./test/TestCases/airports.source.schema.graphql", 6 | "--create-update-apollo-server-neptune-endpoint", ":", 7 | "--output-folder-path", "./test/TestCases/Case09/case09-02-output", 8 | "--create-update-apollo-server-subgraph", 9 | "--output-resolver-query-https"], 10 | "host": "", 11 | "port": "" 12 | } 13 | -------------------------------------------------------------------------------- /test/TestCases/airports.source.schema.graphql: -------------------------------------------------------------------------------- 1 | type Continent @alias(property: "continent") { 2 | id: ID! @id 3 | code: String 4 | type: String 5 | desc: String 6 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) 7 | contains: Contains 8 | } 9 | 10 | input ContinentInput { 11 | id: ID @id 12 | code: String 13 | type: String 14 | desc: String 15 | } 16 | 17 | type Country @alias(property: "country") { 18 | _id: ID! @id 19 | code: String 20 | type: String 21 | desc: String 22 | airportContainssOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "contains", direction: OUT) 23 | contains: Contains 24 | } 25 | 26 | input CountryInput { 27 | _id: ID @id 28 | code: String 29 | type: String 30 | desc: String 31 | } 32 | 33 | type Version @alias(property: "version") { 34 | _id: ID! @id 35 | date: String 36 | code: String 37 | author: String 38 | type: String 39 | desc: String 40 | } 41 | 42 | input VersionInput { 43 | _id: ID @id 44 | date: String 45 | code: String 46 | author: String 47 | type: String 48 | desc: String 49 | } 50 | 51 | type Airport @alias(property: "airport") { 52 | _id: ID! @id 53 | country: String 54 | longest: Int 55 | code: String 56 | city: String 57 | elev: Int 58 | icao: String 59 | lon: Float 60 | runways: Int 61 | region: String 62 | type: String 63 | lat: Float 64 | desc2: String @alias(property: "desc") 65 | outboundRoutesCount: Int @graphQuery(statement: "MATCH (this)-[r:route]->(a) RETURN count(r)") 66 | continentContainsIn: Continent @relationship(edgeType: "contains", direction: IN) 67 | countryContainsIn: Country @relationship(edgeType: "contains", direction: IN) 68 | airportRoutesOut(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: OUT) 69 | airportRoutesIn(filter: AirportInput, options: Options): [Airport] @relationship(edgeType: "route", direction: IN) 70 | contains: Contains 71 | route: Route 72 | } 73 | 74 | input AirportInput { 75 | _id: ID @id 76 | country: String 77 | longest: Int 78 | code: String 79 | city: String 80 | elev: Int 81 | icao: String 82 | lon: Float 83 | runways: Int 84 | region: String 85 | type: String 86 | lat: Float 87 | desc: String 88 | } 89 | 90 | type Contains @alias(property: "contains") { 91 | _id: ID! @id 92 | } 93 | 94 | type Route @alias(property: "route") { 95 | _id: ID! @id 96 | dist: Int 97 | } 98 | 99 | input RouteInput { 100 | dist: Int 101 | } 102 | 103 | input Options { 104 | limit: Int 105 | } 106 | 107 | type Query { 108 | getAirport(code: String): Airport 109 | getAirportConnection(fromCode: String!, toCode: String!): Airport @cypher(statement: "MATCH (:airport{code: '$fromCode'})-[:route]->(this:airport)-[:route]->(:airport{code:'$toCode'})") 110 | getAirportWithGremlin(code:String): Airport @graphQuery(statement: "g.V().has('airport', 'code', '$code').elementMap()") 111 | getContinentsWithGremlin: [Continent] @graphQuery(statement: "g.V().hasLabel('continent').elementMap().fold()") 112 | getCountriesCountGremlin: Int @graphQuery(statement: "g.V().hasLabel('country').count()") 113 | 114 | getNodeContinent(filter: ContinentInput): Continent 115 | getNodeContinents(filter: ContinentInput, options: Options): [Continent] 116 | getNodeCountry(filter: CountryInput): Country 117 | getNodeCountrys(filter: CountryInput, options: Options): [Country] 118 | getNodeVersion(filter: VersionInput): Version 119 | getNodeVersions(filter: VersionInput, options: Options): [Version] 120 | getNodeAirport(filter: AirportInput): Airport 121 | getNodeAirports(filter: AirportInput, options: Options): [Airport] 122 | } 123 | 124 | type Mutation { 125 | createAirport(input: AirportInput!): Airport @graphQuery(statement: "CREATE (this:airport {$input}) RETURN this") 126 | 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") 127 | deleteAirport(id: ID): Int @graphQuery(statement: "MATCH (this:airport) WHERE ID(this) = '$id' DETACH DELETE this") 128 | 129 | createNodeContinent(input: ContinentInput!): Continent 130 | updateNodeContinent(input: ContinentInput!): Continent 131 | deleteNodeContinent(_id: ID!): Boolean 132 | createNodeCountry(input: CountryInput!): Country 133 | updateNodeCountry(input: CountryInput!): Country 134 | deleteNodeCountry(_id: ID!): Boolean 135 | createNodeVersion(input: VersionInput!): Version 136 | updateNodeVersion(input: VersionInput!): Version 137 | deleteNodeVersion(_id: ID!): Boolean 138 | createNodeAirport(input: AirportInput!): Airport 139 | updateNodeAirport(input: AirportInput!): Airport 140 | deleteNodeAirport(_id: ID!): Boolean 141 | connectNodeContinentToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 142 | deleteEdgeContainsFromContinentToAirport(from_id: ID!, to_id: ID!): Boolean 143 | connectNodeCountryToNodeAirportEdgeContains(from_id: ID!, to_id: ID!): Contains 144 | deleteEdgeContainsFromCountryToAirport(from_id: ID!, to_id: ID!): Boolean 145 | connectNodeAirportToNodeAirportEdgeRoute(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 146 | updateEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!, edge: RouteInput!): Route 147 | deleteEdgeRouteFromAirportToAirport(from_id: ID!, to_id: ID!): Boolean 148 | } 149 | 150 | schema { 151 | query: Query 152 | mutation: Mutation 153 | } -------------------------------------------------------------------------------- /test/jestTestSequencer.js: -------------------------------------------------------------------------------- 1 | import Sequencer from '@jest/test-sequencer'; 2 | 3 | export default class CustomSequencer extends Sequencer.default { 4 | sort(tests) { 5 | const copyTests = Array.from(tests); 6 | return copyTests.sort((testA, testB) => (testA.path > testB.path ? 1 : -1)); 7 | } 8 | } -------------------------------------------------------------------------------- /test/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neptune-for-graphql-test", 3 | "version": "0.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "neptune-for-graphql-test", 9 | "version": "0.0.1", 10 | "license": "ISC", 11 | "dependencies": { 12 | "adm-zip": "0.5.16", 13 | "graphql-tag": "2.12.6" 14 | } 15 | }, 16 | "node_modules/adm-zip": { 17 | "version": "0.5.16", 18 | "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", 19 | "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", 20 | "license": "MIT", 21 | "engines": { 22 | "node": ">=12.0" 23 | } 24 | }, 25 | "node_modules/graphql": { 26 | "version": "16.10.0", 27 | "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", 28 | "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", 29 | "license": "MIT", 30 | "peer": true, 31 | "engines": { 32 | "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" 33 | } 34 | }, 35 | "node_modules/graphql-tag": { 36 | "version": "2.12.6", 37 | "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", 38 | "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", 39 | "license": "MIT", 40 | "dependencies": { 41 | "tslib": "^2.1.0" 42 | }, 43 | "engines": { 44 | "node": ">=10" 45 | }, 46 | "peerDependencies": { 47 | "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" 48 | } 49 | }, 50 | "node_modules/tslib": { 51 | "version": "2.8.1", 52 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 53 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 54 | "license": "0BSD" 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "neptune-for-graphql-test", 3 | "version": "0.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "type": "module", 12 | "dependencies": { 13 | "graphql-tag": "2.12.6", 14 | "adm-zip": "0.5.16" 15 | } 16 | } 17 | --------------------------------------------------------------------------------