├── .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 | 
107 |
108 | Here quering all the Todos with *getNodeTodos* query.
109 |
110 | 
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 | 
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 |
--------------------------------------------------------------------------------