diff --git a/.github/workflows/bug-reopened.yml b/.github/workflows/bug-reopened.yml index efa0a9e5e3c..b54a5d3797d 100644 --- a/.github/workflows/bug-reopened.yml +++ b/.github/workflows/bug-reopened.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3 + - uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0 with: project: Bug Triage column: Bug reports diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml index 5072dad78d0..0e10a041562 100644 --- a/.github/workflows/changesets.yml +++ b/.github/workflows/changesets.yml @@ -30,7 +30,7 @@ jobs: persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml index 254377baf70..afb9e30e58b 100644 --- a/.github/workflows/cla-check.yml +++ b/.github/workflows/cla-check.yml @@ -17,7 +17,7 @@ jobs: with: repository: neo-technology/whitelist-check token: ${{ secrets.NEO4J_TEAM_GRAPHQL_PERSONAL_ACCESS_TOKEN }} - - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 + - uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5 with: python-version: 3 - name: Install dependencies diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml index 643d5011d32..470f8e95f0d 100644 --- a/.github/workflows/issue-labeled.yml +++ b/.github/workflows/issue-labeled.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3 + - uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0 with: project: Bug Triage column: Bug reports @@ -46,7 +46,7 @@ jobs: issue-number: ${{ github.event.issue.number }} body: | We've been able to confirm this bug using the steps to reproduce that you provided - many thanks @${{ github.event.issue.user.login }}! :pray: We will now prioritise the bug and address it appropriately. - - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3 + - uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0 with: project: Bug Triage column: Confirmed diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 80d205f614b..b3ad8a5b946 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -13,6 +13,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: srvaroa/labeler@74404350883f8b689b026d8747622bd12d3f070a # v1.8.0 + - uses: srvaroa/labeler@0381dc470140eaebc6fd87fc4aedc4dd2f39f997 # v1.10.0 env: GITHUB_TOKEN: ${{ secrets.NEO4J_TEAM_GRAPHQL_PERSONAL_ACCESS_TOKEN }} diff --git a/.github/workflows/lint-github-actions.yml b/.github/workflows/lint-github-actions.yml index a2ccf0684ab..292b1b71165 100644 --- a/.github/workflows/lint-github-actions.yml +++ b/.github/workflows/lint-github-actions.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: reviewdog/action-actionlint@82693e9e3b239f213108d6e412506f8b54003586 # v1.39.1 + - uses: reviewdog/action-actionlint@9ccda195fd3a290c8596db7f1958c897deaa8c76 # v1.40.0 with: reporter: github-check fail_on_error: true diff --git a/.github/workflows/lint-markdown.yml b/.github/workflows/lint-markdown.yml index 432fe1ccc78..d761150cced 100644 --- a/.github/workflows/lint-markdown.yml +++ b/.github/workflows/lint-markdown.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* - name: Install markdownlint diff --git a/.github/workflows/performance-tests.yml b/.github/workflows/performance-tests.yml index d404e7a2899..6d6f58b1fc2 100644 --- a/.github/workflows/performance-tests.yml +++ b/.github/workflows/performance-tests.yml @@ -5,6 +5,7 @@ on: branches: - dev - master + - "*.*.*" paths: - "packages/graphql/tests/tck/**" @@ -24,7 +25,7 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* diff --git a/.github/workflows/pull-request-labeled.yml b/.github/workflows/pull-request-labeled.yml index 8939bed42d8..c7bfb7e563c 100644 --- a/.github/workflows/pull-request-labeled.yml +++ b/.github/workflows/pull-request-labeled.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3 + - uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0 with: project: RFCs column: RFCs diff --git a/.github/workflows/pull-request-opened.yml b/.github/workflows/pull-request-opened.yml index 57f71debfa2..b4a501e7880 100644 --- a/.github/workflows/pull-request-opened.yml +++ b/.github/workflows/pull-request-opened.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3 + - uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0 with: project: PR Triage column: Pull requests diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index f896438ec48..867125873db 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -9,6 +9,7 @@ on: branches: - dev - master + - "*.*.*" paths-ignore: - "docs/**" @@ -18,13 +19,13 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: 18.13.0 cache: yarn - name: Install dependencies run: yarn --immutable - - uses: reviewdog/action-eslint@10ca150f51dbbb963467c37a03c873ba1fa75f91 # v1.20.0 + - uses: reviewdog/action-eslint@279acb08336462ec76183a2d9ef1dd43e4c6b391 # v1.21.0 with: fail_on_error: true eslint_flags: "." @@ -61,7 +62,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4 + - uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5 with: go-version: "^1.17.0" - name: Install addlicense @@ -88,6 +89,7 @@ jobs: uses: ./.github/workflows/reusable-federation-tests.yml quality-gate: + if: ${{ !(failure() || cancelled()) }} needs: - reviewdog-eslint - code-scanning diff --git a/.github/workflows/reusable-api-library-tests.yml b/.github/workflows/reusable-api-library-tests.yml index 9098da99311..e075e9bda3b 100644 --- a/.github/workflows/reusable-api-library-tests.yml +++ b/.github/workflows/reusable-api-library-tests.yml @@ -48,7 +48,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* cache: yarn @@ -85,7 +85,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* cache: yarn @@ -107,7 +107,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* cache: yarn diff --git a/.github/workflows/reusable-aura-tests.yml b/.github/workflows/reusable-aura-tests.yml index 9ca743d6ce8..eee4496f399 100644 --- a/.github/workflows/reusable-aura-tests.yml +++ b/.github/workflows/reusable-aura-tests.yml @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: ref: ${{ inputs.BRANCH || github.ref }} - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* cache: yarn @@ -64,7 +64,7 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* cache: yarn diff --git a/.github/workflows/reusable-codeql-analysis.yml b/.github/workflows/reusable-codeql-analysis.yml index 72717506d99..60a7ae808d0 100644 --- a/.github/workflows/reusable-codeql-analysis.yml +++ b/.github/workflows/reusable-codeql-analysis.yml @@ -10,13 +10,13 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* - name: Initialize CodeQL - uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2 + uses: github/codeql-action/init@8b7fcbfac2aae0e6c24d9f9ebd5830b1290b18e4 # v2 with: config-file: ./.github/codeql/codeql-config.yml languages: javascript - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2 + uses: github/codeql-action/analyze@8b7fcbfac2aae0e6c24d9f9ebd5830b1290b18e4 # v2 diff --git a/.github/workflows/reusable-federation-tests.yml b/.github/workflows/reusable-federation-tests.yml index dc59fe2c450..ba4aeb37135 100644 --- a/.github/workflows/reusable-federation-tests.yml +++ b/.github/workflows/reusable-federation-tests.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* cache: yarn diff --git a/.github/workflows/reusable-integration-tests-on-prem-nightly.yml b/.github/workflows/reusable-integration-tests-on-prem-nightly.yml index f2d06131ab1..761d3d4a3f6 100644 --- a/.github/workflows/reusable-integration-tests-on-prem-nightly.yml +++ b/.github/workflows/reusable-integration-tests-on-prem-nightly.yml @@ -78,7 +78,7 @@ jobs: - name: Check out repository code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Setting up Node.js with version ${{ matrix.node }} - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: ${{ matrix.node }} cache: yarn diff --git a/.github/workflows/reusable-integration-tests-on-prem.yml b/.github/workflows/reusable-integration-tests-on-prem.yml index 124f7a1815d..a0c995e0018 100644 --- a/.github/workflows/reusable-integration-tests-on-prem.yml +++ b/.github/workflows/reusable-integration-tests-on-prem.yml @@ -53,7 +53,7 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* cache: yarn diff --git a/.github/workflows/reusable-package-tests.yml b/.github/workflows/reusable-package-tests.yml index 98fe65896f2..a7a97ee82b6 100644 --- a/.github/workflows/reusable-package-tests.yml +++ b/.github/workflows/reusable-package-tests.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: 18.13.0 cache: yarn diff --git a/.github/workflows/reusable-subscriptions-plugin-amqp-e2e-test.yml b/.github/workflows/reusable-subscriptions-plugin-amqp-e2e-test.yml index 9864bed2518..d109de47507 100644 --- a/.github/workflows/reusable-subscriptions-plugin-amqp-e2e-test.yml +++ b/.github/workflows/reusable-subscriptions-plugin-amqp-e2e-test.yml @@ -26,7 +26,7 @@ jobs: ports: - 7687:7687 rabbitmq: - image: rabbitmq@sha256:b669305108158abf3d7790a7f6f5a56b7de9598a926b6672281a7edf11460a2d + image: rabbitmq@sha256:56fea741980800b4e4a6db474372bed284c93dad0db2da4c982ad4b163dcb341 env: RABBITMQ_DEFAULT_USER: guest RABBITMQ_DEFAULT_PASS: guest @@ -37,7 +37,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* cache: yarn diff --git a/.github/workflows/reusable-toolbox-tests.yml b/.github/workflows/reusable-toolbox-tests.yml index eef6527dfbf..1b892af32eb 100644 --- a/.github/workflows/reusable-toolbox-tests.yml +++ b/.github/workflows/reusable-toolbox-tests.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* cache: yarn diff --git a/.github/workflows/reusable-unit-tests.yml b/.github/workflows/reusable-unit-tests.yml index 01ca66b6eee..06b05e4ed19 100644 --- a/.github/workflows/reusable-unit-tests.yml +++ b/.github/workflows/reusable-unit-tests.yml @@ -30,7 +30,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* cache: yarn diff --git a/.github/workflows/toolbox-build.yml b/.github/workflows/toolbox-build.yml index 1ea45714b3c..9a1a8902ccc 100644 --- a/.github/workflows/toolbox-build.yml +++ b/.github/workflows/toolbox-build.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* - name: Install dependencies diff --git a/.github/workflows/toolbox-deploy.yml b/.github/workflows/toolbox-deploy.yml index b76547eb176..04362145a56 100644 --- a/.github/workflows/toolbox-deploy.yml +++ b/.github/workflows/toolbox-deploy.yml @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* - name: Install dependencies diff --git a/.github/workflows/toolbox-publish.yml b/.github/workflows/toolbox-publish.yml index 8da0670ccfd..2ecbcef7f50 100644 --- a/.github/workflows/toolbox-publish.yml +++ b/.github/workflows/toolbox-publish.yml @@ -39,7 +39,7 @@ jobs: number=$(> "$GITHUB_OUTPUT" - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* - name: Publish the Toolbox to surge.sh diff --git a/.github/workflows/toolbox-teardown.yml b/.github/workflows/toolbox-teardown.yml index 6015fb2f185..498aeb11fdc 100644 --- a/.github/workflows/toolbox-teardown.yml +++ b/.github/workflows/toolbox-teardown.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 + - uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4 with: node-version: lts/* - name: Teardown graphql-toolbox diff --git a/Dockerfile b/Dockerfile index 14f0cd34a35..03d16673b0d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.10.0-buster-slim@sha256:b46831a79b7bd8d8d38b2bd50273b7611f0e168c840a97a1137a0cf243850086 +FROM node:20.11.0-buster-slim@sha256:045eb1471de28085f2388f7307b87f1b66329ed6335c86dc79e15b4f8705d187 WORKDIR /app diff --git a/examples/neo-place/docker-compose.yml b/examples/neo-place/docker-compose.yml index d578a9e8296..9cfd8faac57 100644 --- a/examples/neo-place/docker-compose.yml +++ b/examples/neo-place/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.5' services: rabbitmq: - image: rabbitmq:3.12-management@sha256:f8237ef62feb8fc61fe949170fa15379c45c784e1b8f1e9a4a0b9fbf7c5c8bf1 + image: rabbitmq:3.12-management@sha256:a44ba5a23397c3d84f8639f1adc037555541877df8fd370ef460133548e21b36 ports: - "5672:5672" - "15672:15672" diff --git a/examples/neo-place/package.json b/examples/neo-place/package.json index f95b5a19fc3..ff4c44d8056 100644 --- a/examples/neo-place/package.json +++ b/examples/neo-place/package.json @@ -51,6 +51,6 @@ }, "devDependencies": { "concurrently": "8.2.2", - "parcel": "2.10.3" + "parcel": "2.11.0" } } diff --git a/examples/subscriptions/apollo_rabbitmq/docker-compose.yml b/examples/subscriptions/apollo_rabbitmq/docker-compose.yml index d578a9e8296..9cfd8faac57 100644 --- a/examples/subscriptions/apollo_rabbitmq/docker-compose.yml +++ b/examples/subscriptions/apollo_rabbitmq/docker-compose.yml @@ -1,7 +1,7 @@ version: '3.5' services: rabbitmq: - image: rabbitmq:3.12-management@sha256:f8237ef62feb8fc61fe949170fa15379c45c784e1b8f1e9a4a0b9fbf7c5c8bf1 + image: rabbitmq:3.12-management@sha256:a44ba5a23397c3d84f8639f1adc037555541877df8fd370ef460133548e21b36 ports: - "5672:5672" - "15672:15672" diff --git a/package.json b/package.json index d3e3c8f3a74..5339623e870 100644 --- a/package.json +++ b/package.json @@ -26,17 +26,17 @@ }, "devDependencies": { "@tsconfig/node16": "1.0.4", - "@typescript-eslint/eslint-plugin": "6.13.1", - "@typescript-eslint/parser": "6.13.1", + "@typescript-eslint/eslint-plugin": "6.18.1", + "@typescript-eslint/parser": "6.18.1", "concurrently": "8.2.2", "dotenv": "16.3.1", - "eslint": "8.55.0", + "eslint": "8.56.0", "eslint-config-prettier": "9.1.0", "eslint-formatter-summary": "1.1.0", "eslint-import-resolver-typescript": "3.6.1", "eslint-plugin-eslint-comments": "3.2.0", - "eslint-plugin-import": "2.29.0", - "eslint-plugin-jest": "27.6.0", + "eslint-plugin-import": "2.29.1", + "eslint-plugin-jest": "27.6.2", "eslint-plugin-jsx-a11y": "6.8.0", "eslint-plugin-react": "7.33.2", "eslint-plugin-simple-import-sort": "10.0.0", @@ -44,7 +44,7 @@ "husky": "8.0.3", "jest": "29.7.0", "lint-staged": "15.2.0", - "neo4j-driver": "5.15.0", + "neo4j-driver": "5.16.0", "npm-run-all": "4.1.5", "prettier": "2.8.8", "set-tz": "0.2.0", diff --git a/packages/apollo-federation-subgraph-compatibility/Dockerfile b/packages/apollo-federation-subgraph-compatibility/Dockerfile index 811732484d7..b84972e8f4a 100644 --- a/packages/apollo-federation-subgraph-compatibility/Dockerfile +++ b/packages/apollo-federation-subgraph-compatibility/Dockerfile @@ -1,4 +1,4 @@ -FROM node:lts@sha256:445acd9b2ef7e9de665424053bf95652e0b8995ef36500557d48faf29300170a +FROM node:lts@sha256:7ba59ab8fe65c91b7d1945e456457c242d2cadaae912ba43ac8e179af6d43183 WORKDIR /app diff --git a/packages/apollo-federation-subgraph-compatibility/package.json b/packages/apollo-federation-subgraph-compatibility/package.json index b5eed4f3cd2..4e12e3fb044 100644 --- a/packages/apollo-federation-subgraph-compatibility/package.json +++ b/packages/apollo-federation-subgraph-compatibility/package.json @@ -10,7 +10,7 @@ "dependencies": { "@apollo/server": "^4.7.0", "@graphql-tools/wrap": "^10.0.0", - "@neo4j/graphql": "^4.4.4", + "@neo4j/graphql": "^4.4.5", "graphql": "16.8.1", "graphql-tag": "^2.12.6", "neo4j-driver": "^5.8.0" diff --git a/packages/graphql-amqp-subscriptions-engine/docker-compose.yml b/packages/graphql-amqp-subscriptions-engine/docker-compose.yml index fbbe8d8c20d..23eb5dfbea2 100644 --- a/packages/graphql-amqp-subscriptions-engine/docker-compose.yml +++ b/packages/graphql-amqp-subscriptions-engine/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.5' # This is just for local testing services: rabbitmq: - image: rabbitmq:3.12-management@sha256:f8237ef62feb8fc61fe949170fa15379c45c784e1b8f1e9a4a0b9fbf7c5c8bf1 + image: rabbitmq:3.12-management@sha256:a44ba5a23397c3d84f8639f1adc037555541877df8fd370ef460133548e21b36 ports: - "5672:5672" - "15672:15672" diff --git a/packages/graphql-amqp-subscriptions-engine/package.json b/packages/graphql-amqp-subscriptions-engine/package.json index aa890ab7886..657023e01ab 100644 --- a/packages/graphql-amqp-subscriptions-engine/package.json +++ b/packages/graphql-amqp-subscriptions-engine/package.json @@ -33,24 +33,24 @@ }, "author": "Neo4j Inc.", "devDependencies": { - "@apollo/server": "4.9.5", + "@apollo/server": "4.10.0", "@neo4j/graphql": "^4.0.0", "@types/amqplib": "0.10.4", "@types/body-parser": "1.19.5", "@types/cors": "2.8.17", "@types/debug": "4.1.12", - "@types/jest": "29.5.10", - "@types/node": "20.10.3", + "@types/jest": "29.5.11", + "@types/node": "20.10.8", "camelcase": "6.3.0", - "graphql-ws": "5.14.2", + "graphql-ws": "5.14.3", "jest": "29.7.0", - "neo4j-driver": "5.15.0", + "neo4j-driver": "5.16.0", "pluralize": "8.0.0", "randomstring": "1.3.0", "supertest": "6.3.3", "ts-jest": "29.1.1", "typescript": "5.1.6", - "ws": "8.14.2" + "ws": "8.16.0" }, "dependencies": { "amqplib": "0.10.3", diff --git a/packages/graphql-amqp-subscriptions-engine/qpid-docker/Dockerfile b/packages/graphql-amqp-subscriptions-engine/qpid-docker/Dockerfile index c23ed74a407..de3be7f65b4 100644 --- a/packages/graphql-amqp-subscriptions-engine/qpid-docker/Dockerfile +++ b/packages/graphql-amqp-subscriptions-engine/qpid-docker/Dockerfile @@ -1,4 +1,4 @@ -FROM ibmjava:8-jre@sha256:956d16bfd2e8f53ab8a9aa0fe82f9750532f8b87a1cfcf6fc27bb3b24deb3726 +FROM ibmjava:8-jre@sha256:c518a629aec5fd4b00334c2ac88622cfb5830ccfab17c194f09f1de582e36ca0 WORKDIR /usr/local/qpid RUN apt-get update && apt-get install -y curl \ && curl https://dlcdn.apache.org/qpid/broker-j/8.0.6/binaries/apache-qpid-broker-j-8.0.6-bin.tar.gz \ diff --git a/packages/graphql-toolbox/CHANGELOG.md b/packages/graphql-toolbox/CHANGELOG.md index 6895d7e984e..3fb09f2c391 100644 --- a/packages/graphql-toolbox/CHANGELOG.md +++ b/packages/graphql-toolbox/CHANGELOG.md @@ -1,5 +1,12 @@ # @neo4j/graphql-toolbox +## 2.1.7 + +### Patch Changes + +- Updated dependencies [[`2bb8f8b`](https://github.com/neo4j/graphql/commit/2bb8f8b628c52c102c2db988685fc54f08521488), [`d3c6d0e`](https://github.com/neo4j/graphql/commit/d3c6d0e8897dfc7ca168f1c387c6c33b92f8cb56), [`5c42f8d`](https://github.com/neo4j/graphql/commit/5c42f8d5226dcc87c804bc752d676fdc9f623d30), [`387e455`](https://github.com/neo4j/graphql/commit/387e455bb9aebb94663b05baed6c8694ec304e80), [`4b97531`](https://github.com/neo4j/graphql/commit/4b97531170c399f243ee848c17a8e38a80e14a51), [`66a19c5`](https://github.com/neo4j/graphql/commit/66a19c56f00be90a8c0c6be294c98d402b0f4c5e)]: + - @neo4j/graphql@4.4.5 + ## 2.1.6 ### Patch Changes diff --git a/packages/graphql-toolbox/package.json b/packages/graphql-toolbox/package.json index 19cfd018540..53dfe073cf0 100644 --- a/packages/graphql-toolbox/package.json +++ b/packages/graphql-toolbox/package.json @@ -1,7 +1,7 @@ { "name": "@neo4j/graphql-toolbox", "private": true, - "version": "2.1.6", + "version": "2.1.7", "description": "Developer UI For Neo4j GraphQL", "exports": "./dist/main.js", "main": "./dist/main.js", @@ -43,26 +43,25 @@ "author": "Neo4j", "dependencies": { "@codemirror/autocomplete": "6.11.1", - "@codemirror/commands": "6.3.2", + "@codemirror/commands": "6.3.3", "@codemirror/lang-javascript": "6.2.1", - "@codemirror/language": "6.9.3", + "@codemirror/language": "6.10.0", "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "7.0.0", "@dnd-kit/sortable": "8.0.0", "@graphiql/react": "0.20.2", - "@neo4j-ndl/base": "2.0.7", - "@neo4j-ndl/react": "2.0.14", - "@neo4j/graphql": "4.4.4", + "@neo4j-ndl/base": "2.3.0", + "@neo4j-ndl/react": "2.3.1", + "@neo4j/graphql": "4.4.5", "@neo4j/introspector": "2.0.0", - "classnames": "2.3.2", + "classnames": "2.5.1", "cm6-graphql": "0.0.12", "codemirror": "6.0.1", "dotenv-webpack": "8.0.1", "graphiql-explorer": "0.9.0", "graphql": "16.8.1", "graphql-query-complexity": "0.12.0", - "markdown-it": "13.0.2", - "neo4j-driver": "5.15.0", + "neo4j-driver": "5.16.0", "prettier": "3.0.0", "process": "0.11.10", "react": "18.2.0", @@ -75,33 +74,32 @@ "@tsconfig/create-react-app": "2.0.1", "@types/codemirror": "5.60.15", "@types/lodash.debounce": "4.0.9", - "@types/markdown-it": "13.0.7", "@types/prettier": "2.7.3", - "@types/react-dom": "18.2.17", + "@types/react-dom": "18.2.18", "@types/webpack": "5.28.5", "autoprefixer": "10.4.16", "compression-webpack-plugin": "10.0.0", "copy-webpack-plugin": "11.0.0", "cross-env": "7.0.3", - "css-loader": "6.8.1", + "css-loader": "6.9.0", "dotenv": "16.3.1", "fork-ts-checker-webpack-plugin": "9.0.2", "html-inline-script-webpack-plugin": "3.2.1", "html-webpack-inline-source-plugin": "0.0.10", - "html-webpack-plugin": "5.5.3", + "html-webpack-plugin": "5.6.0", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "node-polyfill-webpack-plugin": "2.0.1", "parse5": "7.1.2", - "postcss": "8.4.32", - "postcss-loader": "7.3.3", + "postcss": "8.4.33", + "postcss-loader": "7.3.4", "randomstring": "1.3.0", - "style-loader": "3.3.3", - "tailwindcss": "3.3.5", - "terser-webpack-plugin": "5.3.9", + "style-loader": "3.3.4", + "tailwindcss": "3.4.1", + "terser-webpack-plugin": "5.3.10", "ts-jest": "29.1.1", "ts-loader": "9.5.1", - "ts-node": "10.9.1", + "ts-node": "10.9.2", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.1.6", "webpack": "5.89.0", diff --git a/packages/graphql-toolbox/playwright.config.ts b/packages/graphql-toolbox/playwright.config.ts index c9ebee0dba4..b933c4d7bd9 100644 --- a/packages/graphql-toolbox/playwright.config.ts +++ b/packages/graphql-toolbox/playwright.config.ts @@ -17,9 +17,9 @@ * limitations under the License. */ -import { PlaywrightTestConfig, devices } from "@playwright/test"; +import { defineConfig, devices } from "@playwright/test"; -const config: PlaywrightTestConfig = { +export default defineConfig({ webServer: { command: "yarn start", url: "http://localhost:4242", @@ -35,6 +35,7 @@ const config: PlaywrightTestConfig = { forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 6 : undefined, + maxFailures: process.env.CI ? 10 : undefined, projects: [ { name: "chromium", @@ -53,5 +54,4 @@ const config: PlaywrightTestConfig = { }, ], outputDir: "tests/artifacts/", -}; -export default config; +}); diff --git a/packages/graphql-toolbox/src/modules/AppSettings/AppSettings.tsx b/packages/graphql-toolbox/src/modules/AppSettings/AppSettings.tsx index a1e3cfedd68..fe27ffe0885 100644 --- a/packages/graphql-toolbox/src/modules/AppSettings/AppSettings.tsx +++ b/packages/graphql-toolbox/src/modules/AppSettings/AppSettings.tsx @@ -118,7 +118,7 @@ export const AppSettings = ({ onClickClose }: Props) => {
Made by Neo4j, Inc {/* explicitly hard coded values for copyright */} - Copyright © 2002-2023 + Copyright © 2002-2024
App version: 
{process.env.VERSION}
diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 8c79fbfe432..d00f0c62e0f 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,5 +1,66 @@ # @neo4j/graphql +## 4.4.5 + +### Patch Changes + +- [#4449](https://github.com/neo4j/graphql/pull/4449) [`2bb8f8b`](https://github.com/neo4j/graphql/commit/2bb8f8b628c52c102c2db988685fc54f08521488) Thanks [@mjfwebb](https://github.com/mjfwebb)! - Fix: allow non-generated mutations on timestamp fields + + Before this patch, it wasn't possible to update a field with a timestamp directive even when the directive specified that only the UPDATE or CREATE operation field should be generated by the database. + +- [#4375](https://github.com/neo4j/graphql/pull/4375) [`d3c6d0e`](https://github.com/neo4j/graphql/commit/d3c6d0e8897dfc7ca168f1c387c6c33b92f8cb56) Thanks [@angrykoala](https://github.com/angrykoala)! - Add support for logical operators on filters for interfaces under the experimental flag: + + ``` + interface Show { + title: String! + } + + type Movie implements Show { + title: String! + } + + type Series implements Show { + title: String! + } + + ``` + + ``` + query actedInWhere { + shows(where: { OR: [{ title: "Show 1" }, { title: "Show 2" }] }) { + title + } + } + ``` + +- [#4360](https://github.com/neo4j/graphql/pull/4360) [`5c42f8d`](https://github.com/neo4j/graphql/commit/5c42f8d5226dcc87c804bc752d676fdc9f623d30) Thanks [@angrykoala](https://github.com/angrykoala)! - Remove \_on filter for interfaces under experimental flag + +- [#4409](https://github.com/neo4j/graphql/pull/4409) [`387e455`](https://github.com/neo4j/graphql/commit/387e455bb9aebb94663b05baed6c8694ec304e80) Thanks [@MacondoExpress](https://github.com/MacondoExpress)! - Add support for typename_IN filters for interfaces under the experimental flag: + + ``` + interface Show { + title: String! + } + type Movie implements Show { + title: String! + } + type Series implements Show { + title: String! + } + ``` + + ``` + query actedInWhere { + shows(where: { typename_IN: [Series] }) { + title + } + } + ``` + +- [#4483](https://github.com/neo4j/graphql/pull/4483) [`4b97531`](https://github.com/neo4j/graphql/commit/4b97531170c399f243ee848c17a8e38a80e14a51) Thanks [@darrellwarde](https://github.com/darrellwarde)! - Fix authorization variable naming in create operations + +- [#4507](https://github.com/neo4j/graphql/pull/4507) [`66a19c5`](https://github.com/neo4j/graphql/commit/66a19c56f00be90a8c0c6be294c98d402b0f4c5e) Thanks [@mjfwebb](https://github.com/mjfwebb)! - Fixes filtering on nested read operations + ## 4.4.4 ### Patch Changes diff --git a/packages/graphql/jest.config.js b/packages/graphql/jest.config.js index c19d69936ee..4c531e5654a 100644 --- a/packages/graphql/jest.config.js +++ b/packages/graphql/jest.config.js @@ -6,7 +6,6 @@ module.exports = { displayName: "@neo4j/graphql", globalSetup: path.join(__dirname, "jest.global-setup.js"), setupFilesAfterEnv: ["jest-extended/all"], - globalTeardown: path.join(__dirname, "jest.global-teardown.js"), roots: ["/packages/graphql/src/", "/packages/graphql/tests/"], coverageDirectory: "/packages/graphql/coverage/", transform: { diff --git a/packages/graphql/jest.global-setup.js b/packages/graphql/jest.global-setup.js index eac6fe2d261..5024dfeece5 100644 --- a/packages/graphql/jest.global-setup.js +++ b/packages/graphql/jest.global-setup.js @@ -4,6 +4,9 @@ const neo4j = require("neo4j-driver"); const TZ = "Etc/UTC"; const INT_TEST_DB_NAME = "neo4jgraphqlinttestdatabase"; +const cypherDropData = `MATCH (n) DETACH DELETE n`; +const cypherDropIndexes = `CALL apoc.schema.assert({},{},true) YIELD label, key RETURN *`; + module.exports = async function globalSetup() { process.env.NODE_ENV = "test"; @@ -19,12 +22,13 @@ module.exports = async function globalSetup() { let session = null; try { + session = driver.session(); const hasMultiDbSupport = await driver.supportsMultiDb(); if (process.env.USE_DEFAULT_DB || !hasMultiDbSupport) { - // INFO: We do nothing in case the dbms has no multi-db support. + // If we know at this stage that we need to drop data only, then do so + await dropDataAndIndexes(session); return; } - session = driver.session(); await session.run(cypherCreateDb); } catch (error) { if ( @@ -39,11 +43,17 @@ module.exports = async function globalSetup() { ); } else { console.log( - `\nJest /packages/graphql setup: Setup failure on neo4j @ ${NEO_URL}, cypher: "${cypherCreateDb}", Error: ${error.message}` + `\nJest /packages/graphql setup: Failure to create test DB on neo4j @ ${NEO_URL}, cypher: "${cypherCreateDb}", Error: ${error.message}. Falling back to drop data.` ); + await dropDataAndIndexes(session); } } finally { if (session) await session.close(); if (driver) await driver.close(); } }; + +async function dropDataAndIndexes(session) { + await session.run(cypherDropData); + await session.run(cypherDropIndexes); +} diff --git a/packages/graphql/jest.global-teardown.js b/packages/graphql/jest.global-teardown.js deleted file mode 100644 index 0ae368b3a75..00000000000 --- a/packages/graphql/jest.global-teardown.js +++ /dev/null @@ -1,38 +0,0 @@ -const neo4j = require("neo4j-driver"); - -module.exports = async function globalTeardown() { - const { NEO_USER = "neo4j", NEO_PASSWORD = "password", NEO_URL = "neo4j://localhost:7687/neo4j" } = process.env; - const auth = neo4j.auth.basic(NEO_USER, NEO_PASSWORD); - const driver = neo4j.driver(NEO_URL, auth); - const cypherDropDb = `DROP DATABASE ${global.INT_TEST_DB_NAME} IF EXISTS`; - let session = null; - - try { - const hasMultiDbSupport = await driver.supportsMultiDb(); - if (process.env.USE_DEFAULT_DB || !hasMultiDbSupport) { - // INFO: We do nothing in case the dbms has no multi-db support. - return; - } - session = driver.session(); - await session.run(cypherDropDb); - } catch (error) { - if ( - error.message.includes( - "This is an administration command and it should be executed against the system database" - ) || - error.message.includes("Unsupported administration command") || - error.message.includes("Unable to route write operation to leader for database 'system'") - ) { - console.log( - `\nJest /packages/graphql teardown: Expected action - NO drop of database as not supported in the current environment.` - ); - } else { - console.log( - `\nJest /packages/graphql teardown: Teardown failure on neo4j @ ${NEO_URL}, cypher: "${cypherDropDb}", Error: ${error.message}` - ); - } - } finally { - if (session) await session.close(); - if (driver) await driver.close(); - } -}; diff --git a/packages/graphql/package.json b/packages/graphql/package.json index dbdbd406a64..4f7800f36bb 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql", - "version": "4.4.4", + "version": "4.4.5", "description": "A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations", "keywords": [ "neo4j", @@ -49,14 +49,14 @@ }, "author": "Neo4j Inc.", "devDependencies": { - "@apollo/gateway": "2.6.1", - "@apollo/server": "4.9.5", + "@apollo/gateway": "2.6.2", + "@apollo/server": "4.10.0", "@faker-js/faker": "8.3.1", "@types/deep-equal": "1.0.4", "@types/is-uuid": "1.0.2", - "@types/jest": "29.5.10", + "@types/jest": "29.5.11", "@types/jsonwebtoken": "9.0.5", - "@types/node": "20.10.3", + "@types/node": "20.10.8", "@types/pluralize": "0.0.33", "@types/randomstring": "1.1.11", "@types/semver": "7.5.6", @@ -65,15 +65,15 @@ "dedent": "1.5.1", "graphql-middleware": "6.1.35", "graphql-tag": "2.12.6", - "graphql-ws": "5.14.2", + "graphql-ws": "5.14.3", "is-uuid": "1.0.2", "jest": "29.7.0", "jest-extended": "4.0.2", "jwks-rsa": "3.1.0", - "koa": "2.14.2", + "koa": "2.15.0", "koa-jwt": "4.0.4", "koa-router": "12.0.1", - "libnpmsearch": "7.0.0", + "libnpmsearch": "7.0.1", "mock-jwks": "1.0.10", "nock": "13.4.0", "npm-run-all": "4.1.5", @@ -81,9 +81,9 @@ "rimraf": "5.0.5", "supertest": "6.3.3", "ts-jest": "29.1.1", - "ts-node": "10.9.1", + "ts-node": "10.9.2", "typescript": "5.1.6", - "ws": "8.14.2" + "ws": "8.16.0" }, "dependencies": { "@apollo/subgraph": "^2.2.3", @@ -102,8 +102,7 @@ "jose": "^5.0.0", "pluralize": "^8.0.0", "semver": "^7.5.4", - "typescript-memoize": "^1.1.1", - "uuid": "^9.0.0" + "typescript-memoize": "^1.1.1" }, "peerDependencies": { "graphql": "^16.0.0", diff --git a/packages/graphql/src/classes/Exclude.ts b/packages/graphql/src/classes/Exclude.ts deleted file mode 100644 index 7922d5c7050..00000000000 --- a/packages/graphql/src/classes/Exclude.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export interface ExcludeConstructor { - operations: string[]; -} - -class Exclude { - public operations: string[]; - - constructor(input: ExcludeConstructor) { - this.operations = input.operations; - } -} - -export default Exclude; diff --git a/packages/graphql/src/classes/Executor.ts b/packages/graphql/src/classes/Executor.ts index 0249985fcd2..d805f4d1b82 100644 --- a/packages/graphql/src/classes/Executor.ts +++ b/packages/graphql/src/classes/Executor.ts @@ -206,7 +206,12 @@ export class Executor { if (info) { const source = { // We avoid using print here, when possible, as it is a heavy process - query: info.operation.loc?.source.body || print(info.operation), + query: + info.operation.loc?.source.body || + // Print both fragments and operation, otherwise printed queries are invalid due to missing fragments + [Object.values(info.fragments).map((fragment) => print(fragment)), print(info.operation)].join( + "\n\n" + ), params: info.variableValues, }; diff --git a/packages/graphql/src/classes/Neo4jGraphQL.ts b/packages/graphql/src/classes/Neo4jGraphQL.ts index b6d799cfc9c..2ed6ec0819a 100644 --- a/packages/graphql/src/classes/Neo4jGraphQL.ts +++ b/packages/graphql/src/classes/Neo4jGraphQL.ts @@ -285,6 +285,7 @@ class Neo4jGraphQL { features: this.features, authorization: this.authorization, jwtPayloadFieldsMap: this.jwtFieldsMap, + experimental: this.experimental, }; const resolversComposition = { diff --git a/packages/graphql/src/classes/Node.ts b/packages/graphql/src/classes/Node.ts index edc2262594c..bc8f25ce884 100644 --- a/packages/graphql/src/classes/Node.ts +++ b/packages/graphql/src/classes/Node.ts @@ -37,12 +37,10 @@ import type { import type { DecodedGlobalId } from "../utils/global-ids"; import { fromGlobalId, toGlobalId } from "../utils/global-ids"; import { upperFirst } from "../utils/upper-first"; -import type Exclude from "./Exclude"; import type { GraphElementConstructor } from "./GraphElement"; import { GraphElement } from "./GraphElement"; import type { NodeDirective } from "./NodeDirective"; import type { LimitDirective } from "./LimitDirective"; -import type { SchemaConfiguration } from "../schema/schema-configuration"; import { leadingUnderscores } from "../utils/leading-underscore"; import type { Neo4jGraphQLContext } from "../types/neo4j-graphql-context"; @@ -64,8 +62,6 @@ export interface NodeConstructor extends GraphElementConstructor { pointFields: PointField[]; plural?: string; fulltextDirective?: FullText; - exclude?: Exclude; - schemaConfiguration?: SchemaConfiguration; nodeDirective?: NodeDirective; description?: string; limitDirective?: LimitDirective; @@ -129,8 +125,6 @@ class Node extends GraphElement { public interfaceFields: InterfaceField[]; public interfaces: NamedTypeNode[]; public objectFields: ObjectField[]; - public exclude?: Exclude; - public schemaConfiguration?: SchemaConfiguration; public nodeDirective?: NodeDirective; public fulltextDirective?: FullText; public description?: string; @@ -152,8 +146,6 @@ class Node extends GraphElement { this.interfaceFields = input.interfaceFields; this.interfaces = input.interfaces; this.objectFields = input.objectFields; - this.exclude = input.exclude; - this.schemaConfiguration = input.schemaConfiguration; this.nodeDirective = input.nodeDirective; this.fulltextDirective = input.fulltextDirective; this.limit = input.limitDirective; diff --git a/packages/graphql/src/classes/index.ts b/packages/graphql/src/classes/index.ts index 75f7b0ec171..64da04bf09d 100644 --- a/packages/graphql/src/classes/index.ts +++ b/packages/graphql/src/classes/index.ts @@ -18,7 +18,6 @@ */ export * from "./Error"; -export { default as Exclude, ExcludeConstructor } from "./Exclude"; export { GraphElement } from "./GraphElement"; export { Neo4jDatabaseInfo } from "./Neo4jDatabaseInfo"; export { default as Neo4jGraphQL, Neo4jGraphQLConstructor } from "./Neo4jGraphQL"; diff --git a/packages/graphql/src/graphql/directives/arguments/enums/CallbackOperation.ts b/packages/graphql/src/graphql/directives/arguments/enums/CallbackOperation.ts deleted file mode 100644 index 2a532381262..00000000000 --- a/packages/graphql/src/graphql/directives/arguments/enums/CallbackOperation.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { GraphQLEnumType } from "graphql"; - -/** Deprecated in favour of @populatedBy */ -export const CallbackOperationEnum = new GraphQLEnumType({ - name: "CallbackOperation", - description: "*For use in the @callback directive only*", - values: { - CREATE: {}, - UPDATE: {}, - }, -}); diff --git a/packages/graphql/src/graphql/directives/arguments/enums/ExcludeOperation.ts b/packages/graphql/src/graphql/directives/arguments/enums/ExcludeOperation.ts deleted file mode 100644 index b8f953ebffd..00000000000 --- a/packages/graphql/src/graphql/directives/arguments/enums/ExcludeOperation.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { GraphQLEnumType } from "graphql"; - -export const ExcludeOperationEnum = new GraphQLEnumType({ - name: "ExcludeOperation", - description: "*For use in the @exclude directive only*", - values: { - CREATE: {}, - READ: {}, - UPDATE: {}, - DELETE: {}, - SUBSCRIBE: {}, - }, -}); diff --git a/packages/graphql/src/index.ts b/packages/graphql/src/index.ts index 465ec689cac..9d41079c396 100644 --- a/packages/graphql/src/index.ts +++ b/packages/graphql/src/index.ts @@ -19,7 +19,6 @@ import { Neo4jGraphQL, Neo4jGraphQLConstructor } from "./classes"; import { Neo4jGraphQLContext } from "./types/neo4j-graphql-context"; - import * as directives from "./graphql/directives"; import { CartesianPoint } from "./graphql/objects/CartesianPoint"; import { Point } from "./graphql/objects/Point"; diff --git a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts index 8a4be357070..9823e02575a 100644 --- a/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts +++ b/packages/graphql/src/schema-model/attribute/model-adapters/AttributeAdapter.ts @@ -156,12 +156,23 @@ export class AttributeAdapter { } isOnCreateField(): boolean { + if (!this.isNonGeneratedField()) { + return false; + } + + if (this.annotations.settable?.onCreate === false) { + return false; + } + + if (this.timestampCreateIsGenerated()) { + return false; + } + return ( - this.isNonGeneratedField() && - (this.typeHelper.isScalar() || - this.typeHelper.isSpatial() || - this.typeHelper.isEnum() || - this.typeHelper.isAbstract()) + this.typeHelper.isScalar() || + this.typeHelper.isSpatial() || + this.typeHelper.isEnum() || + this.typeHelper.isAbstract() ); } @@ -192,22 +203,65 @@ export class AttributeAdapter { } isCreateInputField(): boolean { - return this.isNonGeneratedField() && this.annotations.settable?.onCreate !== false; + return ( + this.isNonGeneratedField() && + this.annotations.settable?.onCreate !== false && + !this.timestampCreateIsGenerated() + ); } - isNonGeneratedField(): boolean { + isUpdateInputField(): boolean { return ( - this.isCypher() === false && - this.isCustomResolvable() === false && - (this.typeHelper.isEnum() || this.typeHelper.isScalar() || this.typeHelper.isSpatial()) && - !this.annotations.id && - !this.annotations.populatedBy && - !this.annotations.timestamp + this.isNonGeneratedField() && + this.annotations.settable?.onUpdate !== false && + !this.timestampUpdateIsGenerated() ); } - isUpdateInputField(): boolean { - return this.isNonGeneratedField() && this.annotations.settable?.onUpdate !== false; + timestampCreateIsGenerated(): boolean { + if (!this.annotations.timestamp) { + // The timestamp directive is not set on the field + return false; + } + + if (this.annotations.timestamp.operations.includes("CREATE")) { + // The timestamp directive is set to generate on create + return true; + } + + // The timestamp directive is not set to generate on create + return false; + } + + isNonGeneratedField(): boolean { + if (this.isCypher() || this.isCustomResolvable()) { + return false; + } + + if (this.annotations.id || this.annotations.populatedBy) { + return false; + } + + if (this.typeHelper.isEnum() || this.typeHelper.isScalar() || this.typeHelper.isSpatial()) { + return true; + } + + return false; + } + + timestampUpdateIsGenerated(): boolean { + if (!this.annotations.timestamp) { + // The timestamp directive is not set on the field + return false; + } + + if (this.annotations.timestamp.operations.includes("UPDATE")) { + // The timestamp directive is set to generate on update + return true; + } + + // The timestamp directive is not set to generate on update + return false; } isArrayMethodField(): boolean { diff --git a/packages/graphql/src/schema-model/entity/model-adapters/InterfaceEntityOperations.ts b/packages/graphql/src/schema-model/entity/model-adapters/InterfaceEntityOperations.ts index be896ff4ec3..266ec9ba5f0 100644 --- a/packages/graphql/src/schema-model/entity/model-adapters/InterfaceEntityOperations.ts +++ b/packages/graphql/src/schema-model/entity/model-adapters/InterfaceEntityOperations.ts @@ -88,6 +88,10 @@ export class InterfaceEntityOperations { return `${this.InterfaceEntityAdapter.name}Where`; } + public get implementationEnumTypename(): string { + return `${this.InterfaceEntityAdapter.name}Implementation`; + } + public get whereOnImplementationsWhereInputTypeName(): string { return `${this.InterfaceEntityAdapter.name}ImplementationsWhere`; } diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.test.ts b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.test.ts index becc917c0e0..3d356a52ee1 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.test.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.test.ts @@ -18,27 +18,29 @@ */ import { makeDirectiveNode } from "@graphql-tools/utils"; -import { parseTimestampAnnotation } from "./timestamp-annotation"; import { timestampDirective } from "../../../graphql/directives"; +import { parseTimestampAnnotation } from "./timestamp-annotation"; const tests = [ { name: "should parse correctly when CREATE operation is passed", directive: makeDirectiveNode("timestamp", { operations: ["CREATE"] }, timestampDirective), operations: ["CREATE"], - expected: { operations: ["CREATE"] }, }, { name: "should parse correctly when UPDATE operation is passed", directive: makeDirectiveNode("timestamp", { operations: ["UPDATE"] }, timestampDirective), operations: ["UPDATE"], - expected: { operations: ["UPDATE"] }, }, { name: "should parse correctly when CREATE and UPDATE operations are passed", directive: makeDirectiveNode("timestamp", { operations: ["CREATE", "UPDATE"] }, timestampDirective), operations: ["CREATE", "UPDATE"], - expected: { operations: ["CREATE", "UPDATE"] }, + }, + { + name: "should parse correctly when CREATE and UPDATE operations are passed", + directive: makeDirectiveNode("timestamp", { operations: [] }, timestampDirective), + operations: ["CREATE", "UPDATE"], }, ]; diff --git a/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts index 739de85d914..88b8f7cd1ee 100644 --- a/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts +++ b/packages/graphql/src/schema-model/parser/annotations-parser/timestamp-annotation.ts @@ -24,6 +24,10 @@ import { parseArguments } from "../parse-arguments"; export function parseTimestampAnnotation(directive: DirectiveNode): TimestampAnnotation { const { operations } = parseArguments<{ operations: string[] }>(timestampDirective, directive); + if (operations.length === 0) { + operations.push("CREATE", "UPDATE"); + } + return new TimestampAnnotation({ operations, }); diff --git a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts index e1abcf3bb56..135d98e97a2 100644 --- a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipAdapter.ts @@ -258,13 +258,9 @@ export class RelationshipAdapter { return this.nestedOperations.size > 0 && !onlyConnectOrCreateAndNoUniqueFields; } - public get nonGeneratedProperties(): AttributeAdapter[] { - return Array.from(this.attributes.values()).filter((attribute) => attribute.isNonGeneratedField()); + public get hasNonNullCreateInputFields(): boolean { + return this.createInputFields.some((property) => property.typeHelper.isRequired()); } - public get hasNonNullNonGeneratedProperties(): boolean { - return this.nonGeneratedProperties.some((property) => property.typeHelper.isRequired()); - } - /** * Categories * = a grouping of attributes diff --git a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipOperations.ts b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipOperations.ts index 345b8b2fb84..ba5e1e398a6 100644 --- a/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipOperations.ts +++ b/packages/graphql/src/schema-model/relationship/model-adapters/RelationshipOperations.ts @@ -236,7 +236,7 @@ export class RelationshipOperations { public get edgeCreateInputTypeName(): string { return `${this.relationship.propertiesTypeName}CreateInput${ - this.relationship.hasNonNullNonGeneratedProperties ? `!` : "" + this.relationship.hasNonNullCreateInputFields ? `!` : "" }`; } public get createInputTypeName(): string { diff --git a/packages/graphql/src/schema/create-global-nodes.ts b/packages/graphql/src/schema/create-global-nodes.ts index 456caaf164e..b2bbe1381d5 100644 --- a/packages/graphql/src/schema/create-global-nodes.ts +++ b/packages/graphql/src/schema/create-global-nodes.ts @@ -38,7 +38,7 @@ export function addGlobalNodeFields( if (globalNodes.length === 0) return false; const fetchById = (id: string, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) => { - const resolver = globalNodeResolver({ nodes: globalNodes, entities: globalEntities }); + const resolver = globalNodeResolver({ entities: globalEntities }); return resolver.resolve(null, { id }, context, info); }; diff --git a/packages/graphql/src/schema/create-relationship-fields/create-connect-or-create-field.ts b/packages/graphql/src/schema/create-relationship-fields/create-connect-or-create-field.ts index d1829c758c8..4523b7da37e 100644 --- a/packages/graphql/src/schema/create-relationship-fields/create-connect-or-create-field.ts +++ b/packages/graphql/src/schema/create-relationship-fields/create-connect-or-create-field.ts @@ -51,13 +51,13 @@ export function createOnCreateITC({ const onCreateFields: InputTypeComposerFieldConfigMapDefinition = { node: onCreateInput.NonNull, }; - if (relationshipAdapter.nonGeneratedProperties.length > 0) { + if (relationshipAdapter.createInputFields.length > 0) { const edgeFieldType = withCreateInputType({ entityAdapter: relationshipAdapter, userDefinedFieldDirectives, composer: schemaComposer, }); - onCreateFields["edge"] = relationshipAdapter.hasNonNullNonGeneratedProperties + onCreateFields["edge"] = relationshipAdapter.hasNonNullCreateInputFields ? edgeFieldType.NonNull : edgeFieldType; } diff --git a/packages/graphql/src/schema/create-relationship-fields/create-relationship-concrete-fields.ts b/packages/graphql/src/schema/create-relationship-fields/create-relationship-concrete-fields.ts new file mode 100644 index 00000000000..cbf4bc71470 --- /dev/null +++ b/packages/graphql/src/schema/create-relationship-fields/create-relationship-concrete-fields.ts @@ -0,0 +1,100 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DirectiveNode } from "graphql"; +import type { Directive, InterfaceTypeComposer, ObjectTypeComposer, SchemaComposer } from "graphql-compose"; +import type { Subgraph } from "../../classes/Subgraph"; +import type { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { augmentObjectOrInterfaceTypeWithRelationshipField } from "../generation/augment-object-or-interface"; +import { augmentConnectInputTypeWithConnectFieldInput } from "../generation/connect-input"; +import { withConnectOrCreateInputType } from "../generation/connect-or-create-input"; +import { augmentCreateInputTypeWithRelationshipsInput } from "../generation/create-input"; +import { augmentDeleteInputTypeWithDeleteFieldInput } from "../generation/delete-input"; +import { augmentDisconnectInputTypeWithDisconnectFieldInput } from "../generation/disconnect-input"; +import { withRelationInputType } from "../generation/relation-input"; +import { augmentUpdateInputTypeWithUpdateFieldInput } from "../generation/update-input"; +import { withSourceWhereInputType } from "../generation/where-input"; + +export function createRelationshipConcreteFields({ + relationshipAdapter, + composeNode, + schemaComposer, + userDefinedFieldDirectives, + deprecatedDirectives, + subgraph, +}: { + relationshipAdapter: RelationshipAdapter; + composeNode: ObjectTypeComposer | InterfaceTypeComposer; + schemaComposer: SchemaComposer; + userDefinedFieldDirectives: Map; + deprecatedDirectives: Directive[]; + subgraph?: Subgraph; +}) { + withSourceWhereInputType({ relationshipAdapter, composer: schemaComposer, deprecatedDirectives }); + + withConnectOrCreateInputType({ + relationshipAdapter, + composer: schemaComposer, + userDefinedFieldDirectives, + deprecatedDirectives, + }); + + composeNode.addFields( + augmentObjectOrInterfaceTypeWithRelationshipField(relationshipAdapter, userDefinedFieldDirectives, subgraph) + ); + + withRelationInputType({ + relationshipAdapter, + composer: schemaComposer, + deprecatedDirectives, + userDefinedFieldDirectives, + }); + + augmentCreateInputTypeWithRelationshipsInput({ + relationshipAdapter, + composer: schemaComposer, + deprecatedDirectives, + userDefinedFieldDirectives, + }); + + augmentConnectInputTypeWithConnectFieldInput({ + relationshipAdapter, + composer: schemaComposer, + deprecatedDirectives, + }); + + augmentUpdateInputTypeWithUpdateFieldInput({ + relationshipAdapter, + composer: schemaComposer, + deprecatedDirectives, + userDefinedFieldDirectives, + }); + + augmentDeleteInputTypeWithDeleteFieldInput({ + relationshipAdapter, + composer: schemaComposer, + deprecatedDirectives, + }); + + augmentDisconnectInputTypeWithDisconnectFieldInput({ + relationshipAdapter, + composer: schemaComposer, + deprecatedDirectives, + }); +} diff --git a/packages/graphql/src/schema/create-relationship-fields/create-relationship-fields.ts b/packages/graphql/src/schema/create-relationship-fields/create-relationship-fields.ts index 4f35b54fadd..d6627f00b69 100644 --- a/packages/graphql/src/schema/create-relationship-fields/create-relationship-fields.ts +++ b/packages/graphql/src/schema/create-relationship-fields/create-relationship-fields.ts @@ -22,21 +22,13 @@ import type { Directive, InterfaceTypeComposer, SchemaComposer } from "graphql-c import { ObjectTypeComposer } from "graphql-compose"; import type { Subgraph } from "../../classes/Subgraph"; import { DEPRECATED } from "../../constants"; -import type { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import { ConcreteEntityAdapter } from "../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { InterfaceEntityAdapter } from "../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; import { UnionEntityAdapter } from "../../schema-model/entity/model-adapters/UnionEntityAdapter"; import { FieldAggregationComposer } from "../aggregations/field-aggregation-composer"; import { addDirectedArgument } from "../directed-argument"; -import { augmentObjectOrInterfaceTypeWithRelationshipField } from "../generation/augment-object-or-interface"; -import { augmentConnectInputTypeWithConnectFieldInput } from "../generation/connect-input"; -import { withConnectOrCreateInputType } from "../generation/connect-or-create-input"; -import { augmentCreateInputTypeWithRelationshipsInput } from "../generation/create-input"; -import { augmentDeleteInputTypeWithDeleteFieldInput } from "../generation/delete-input"; -import { augmentDisconnectInputTypeWithDisconnectFieldInput } from "../generation/disconnect-input"; -import { withRelationInputType } from "../generation/relation-input"; -import { augmentUpdateInputTypeWithUpdateFieldInput } from "../generation/update-input"; -import { withSourceWhereInputType } from "../generation/where-input"; import { graphqlDirectivesToCompose } from "../to-compose"; +import { createRelationshipConcreteFields } from "./create-relationship-concrete-fields"; import { createRelationshipInterfaceFields } from "./create-relationship-interface-fields"; import { createRelationshipUnionFields } from "./create-relationship-union-fields"; @@ -70,10 +62,11 @@ export function createRelationshipFields({ if (relationshipTarget instanceof UnionEntityAdapter) { createRelationshipUnionFields({ - relationship: relationshipAdapter, + relationshipAdapter, composeNode, schemaComposer, userDefinedFieldDirectives, + experimental, }); return; @@ -87,6 +80,7 @@ export function createRelationshipFields({ ); } + // NOTE: Non-experimental path for InterfaceEntityAdapter if (!experimental && relationshipTarget instanceof InterfaceEntityAdapter) { createRelationshipInterfaceFields({ relationship: relationshipAdapter, @@ -121,6 +115,7 @@ export function createRelationshipFields({ } } + // NOTE: Experimental path for InterfaceEntityAdapter // Specifically placed the check for InterfaceEntityAdapter here // so that we exit the function at this point, after the aggregation fields have been added above if (relationshipTarget instanceof InterfaceEntityAdapter) { @@ -134,60 +129,15 @@ export function createRelationshipFields({ return; } - // ======== only on relationships to concrete entities: - withSourceWhereInputType({ relationshipAdapter, composer: schemaComposer, deprecatedDirectives }); - - // ======== only on relationships to concrete | unions: - // TODO: refactor - withConnectOrCreateInputType({ - relationshipAdapter, - composer: schemaComposer, - userDefinedFieldDirectives, - deprecatedDirectives, - }); - - // ======== all relationships: - composeNode.addFields( - augmentObjectOrInterfaceTypeWithRelationshipField(relationshipAdapter, userDefinedFieldDirectives, subgraph) - ); - - withRelationInputType({ - relationshipAdapter, - composer: schemaComposer, - deprecatedDirectives, - userDefinedFieldDirectives, - }); - - augmentCreateInputTypeWithRelationshipsInput({ - relationshipAdapter, - composer: schemaComposer, - deprecatedDirectives, - userDefinedFieldDirectives, - }); - - augmentConnectInputTypeWithConnectFieldInput({ - relationshipAdapter, - composer: schemaComposer, - deprecatedDirectives, - }); - - augmentUpdateInputTypeWithUpdateFieldInput({ - relationshipAdapter, - composer: schemaComposer, - deprecatedDirectives, - userDefinedFieldDirectives, - }); - - augmentDeleteInputTypeWithDeleteFieldInput({ - relationshipAdapter, - composer: schemaComposer, - deprecatedDirectives, - }); - - augmentDisconnectInputTypeWithDisconnectFieldInput({ - relationshipAdapter, - composer: schemaComposer, - deprecatedDirectives, - }); + if (relationshipTarget instanceof ConcreteEntityAdapter) { + createRelationshipConcreteFields({ + relationshipAdapter, + composeNode, + schemaComposer, + userDefinedFieldDirectives, + deprecatedDirectives, + subgraph, + }); + } }); } diff --git a/packages/graphql/src/schema/create-relationship-fields/create-relationship-union-fields.ts b/packages/graphql/src/schema/create-relationship-fields/create-relationship-union-fields.ts index e98b9443bbe..0e96a3e7d36 100644 --- a/packages/graphql/src/schema/create-relationship-fields/create-relationship-union-fields.ts +++ b/packages/graphql/src/schema/create-relationship-fields/create-relationship-union-fields.ts @@ -18,7 +18,7 @@ */ import type { DirectiveNode } from "graphql"; -import type { ObjectTypeComposer, SchemaComposer, InterfaceTypeComposer } from "graphql-compose"; +import type { InterfaceTypeComposer, ObjectTypeComposer, SchemaComposer } from "graphql-compose"; import type { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { augmentObjectOrInterfaceTypeWithRelationshipField } from "../generation/augment-object-or-interface"; import { augmentConnectInputTypeWithConnectFieldInput } from "../generation/connect-input"; @@ -28,64 +28,74 @@ import { augmentDeleteInputTypeWithDeleteFieldInput } from "../generation/delete import { augmentDisconnectInputTypeWithDisconnectFieldInput } from "../generation/disconnect-input"; import { withRelationInputType } from "../generation/relation-input"; import { augmentUpdateInputTypeWithUpdateFieldInput } from "../generation/update-input"; +import { withSourceWhereInputType } from "../generation/where-input"; export function createRelationshipUnionFields({ - relationship, + relationshipAdapter, composeNode, schemaComposer, userDefinedFieldDirectives, + experimental, }: { - relationship: RelationshipAdapter; + relationshipAdapter: RelationshipAdapter; composeNode: ObjectTypeComposer | InterfaceTypeComposer; schemaComposer: SchemaComposer; userDefinedFieldDirectives: Map; + experimental: boolean; }) { + if (experimental) { + // ======== only on relationships to concrete | unions: + withSourceWhereInputType({ relationshipAdapter, composer: schemaComposer, deprecatedDirectives: [] }); + } + // ======== only on relationships to concrete | unions: withConnectOrCreateInputType({ - relationshipAdapter: relationship, + relationshipAdapter, composer: schemaComposer, userDefinedFieldDirectives, deprecatedDirectives: [], }); // ======== all relationships: - composeNode.addFields(augmentObjectOrInterfaceTypeWithRelationshipField(relationship, userDefinedFieldDirectives)); + composeNode.addFields( + augmentObjectOrInterfaceTypeWithRelationshipField(relationshipAdapter, userDefinedFieldDirectives) + ); withRelationInputType({ - relationshipAdapter: relationship, + relationshipAdapter, composer: schemaComposer, deprecatedDirectives: [], userDefinedFieldDirectives, }); augmentCreateInputTypeWithRelationshipsInput({ - relationshipAdapter: relationship, + relationshipAdapter, composer: schemaComposer, deprecatedDirectives: [], userDefinedFieldDirectives, }); augmentUpdateInputTypeWithUpdateFieldInput({ - relationshipAdapter: relationship, + relationshipAdapter, composer: schemaComposer, deprecatedDirectives: [], userDefinedFieldDirectives, }); augmentConnectInputTypeWithConnectFieldInput({ - relationshipAdapter: relationship, + relationshipAdapter, composer: schemaComposer, deprecatedDirectives: [], }); augmentDeleteInputTypeWithDeleteFieldInput({ - relationshipAdapter: relationship, + relationshipAdapter, composer: schemaComposer, deprecatedDirectives: [], }); augmentDisconnectInputTypeWithDisconnectFieldInput({ - relationshipAdapter: relationship, + relationshipAdapter, composer: schemaComposer, deprecatedDirectives: [], }); diff --git a/packages/graphql/src/schema/generation/connect-input.ts b/packages/graphql/src/schema/generation/connect-input.ts index 9bab884ed7c..f595b914237 100644 --- a/packages/graphql/src/schema/generation/connect-input.ts +++ b/packages/graphql/src/schema/generation/connect-input.ts @@ -220,7 +220,7 @@ function makeConnectFieldInputTypeFields({ ifUnionMemberEntity?: ConcreteEntityAdapter; }): InputTypeComposerFieldConfigMapDefinition { const fields = {}; - const hasNonGeneratedProperties = relationshipAdapter.nonGeneratedProperties.length > 0; + const hasNonGeneratedProperties = relationshipAdapter.createInputFields.length > 0; if (hasNonGeneratedProperties) { fields["edge"] = relationshipAdapter.operations.edgeCreateInputTypeName; } diff --git a/packages/graphql/src/schema/generation/connect-or-create-input.ts b/packages/graphql/src/schema/generation/connect-or-create-input.ts index 8d7095fd52e..eb83916de86 100644 --- a/packages/graphql/src/schema/generation/connect-or-create-input.ts +++ b/packages/graphql/src/schema/generation/connect-or-create-input.ts @@ -128,6 +128,7 @@ export function withConnectOrCreateInputType({ return connectOrCreateInput; } + function makeConnectOrCreateInputType({ relationshipAdapter, composer, @@ -204,6 +205,7 @@ function withRelationshipConnectOrCreateInputType({ const connectOrCreateInput = composer.createInputTC({ name: typeName, fields }); return connectOrCreateInput; } + function makeUnionConnectOrCreateInputTypeFields({ relationshipAdapter, composer, diff --git a/packages/graphql/src/schema/generation/relation-input.ts b/packages/graphql/src/schema/generation/relation-input.ts index ca5d0ebb47c..fa7a622b8d3 100644 --- a/packages/graphql/src/schema/generation/relation-input.ts +++ b/packages/graphql/src/schema/generation/relation-input.ts @@ -224,7 +224,7 @@ function makeCreateFieldInputTypeFields({ userDefinedFieldDirectives: Map; }): InputTypeComposerFieldConfigMapDefinition { const fields = {}; - const hasNonGeneratedProperties = relationshipAdapter.nonGeneratedProperties.length > 0; + const hasNonGeneratedProperties = relationshipAdapter.createInputFields.length > 0; if (hasNonGeneratedProperties) { fields["edge"] = relationshipAdapter.operations.edgeCreateInputTypeName; } diff --git a/packages/graphql/src/schema/generation/update-input.ts b/packages/graphql/src/schema/generation/update-input.ts index 2616303bf83..5a89343bf53 100644 --- a/packages/graphql/src/schema/generation/update-input.ts +++ b/packages/graphql/src/schema/generation/update-input.ts @@ -32,12 +32,12 @@ import { UnionEntityAdapter } from "../../schema-model/entity/model-adapters/Uni import { RelationshipAdapter } from "../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { concreteEntityToUpdateInputFields, withArrayOperators, withMathOperators } from "../to-compose"; import { withConnectFieldInputType } from "./connect-input"; -import { withDisconnectFieldInputType } from "./disconnect-input"; +import { withConnectOrCreateFieldInputType } from "./connect-or-create-input"; import { withDeleteFieldInputType } from "./delete-input"; +import { withDisconnectFieldInputType } from "./disconnect-input"; import { makeImplementationsUpdateInput } from "./implementation-inputs"; -import { makeConnectionWhereInputType } from "./where-input"; -import { withConnectOrCreateFieldInputType } from "./connect-or-create-input"; import { withCreateFieldInputType } from "./relation-input"; +import { makeConnectionWhereInputType } from "./where-input"; export function withUpdateInputType({ entityAdapter, @@ -428,7 +428,7 @@ function makeUpdateConnectionFieldInputTypeFields({ // fields["node"] = updateInputType; fields["node"] = relationshipAdapter.target.operations.updateInputTypeName; } - const hasNonGeneratedProperties = relationshipAdapter.nonGeneratedProperties.length > 0; + const hasNonGeneratedProperties = relationshipAdapter.updateInputFields.length > 0; if (hasNonGeneratedProperties) { fields["edge"] = relationshipAdapter.operations.edgeUpdateInputTypeName; } diff --git a/packages/graphql/src/schema/generation/where-input.ts b/packages/graphql/src/schema/generation/where-input.ts index 5a42fcfbddd..1f2b5158d4a 100644 --- a/packages/graphql/src/schema/generation/where-input.ts +++ b/packages/graphql/src/schema/generation/where-input.ts @@ -60,11 +60,13 @@ export function withWhereInputType({ userDefinedFieldDirectives, features, composer, + experimental, }: { entityAdapter: EntityAdapter | RelationshipAdapter; userDefinedFieldDirectives: Map; features: Neo4jFeaturesSettings | undefined; composer: SchemaComposer; + experimental: boolean; }): InputTypeComposer { if (composer.has(entityAdapter.operations.whereInputTypeName)) { return composer.getITC(entityAdapter.operations.whereInputTypeName); @@ -91,12 +93,32 @@ export function withWhereInputType({ NOT: whereInputType, }); } else if (entityAdapter instanceof InterfaceEntityAdapter) { - const implementationsWhereInputType = makeImplementationsWhereInput({ - interfaceEntityAdapter: entityAdapter, - composer, - }); - // TODO: add interfaces that implement this interface here - whereInputType.addFields({ _on: implementationsWhereInputType }); + if (experimental) { + whereInputType.addFields({ + OR: whereInputType.NonNull.List, + AND: whereInputType.NonNull.List, + NOT: whereInputType, + }); + + const enumValues = Object.fromEntries( + entityAdapter.concreteEntities.map((concreteEntity) => [ + concreteEntity.name, + { value: concreteEntity.name }, + ]) + ); + const interfaceImplementation = composer.createEnumTC({ + name: entityAdapter.operations.implementationEnumTypename, + values: enumValues, + }); + whereInputType.addFields({ typename_IN: { type: interfaceImplementation.NonNull.List } }); + } else { + const implementationsWhereInputType = makeImplementationsWhereInput({ + interfaceEntityAdapter: entityAdapter, + composer, + }); + // TODO: add interfaces that implement this interface here + whereInputType.addFields({ _on: implementationsWhereInputType }); + } } return whereInputType; } @@ -135,22 +157,25 @@ export function withSourceWhereInputType({ deprecatedDirectives: Directive[]; }): InputTypeComposer | undefined { const relationshipTarget = relationshipAdapter.target; - if (!(relationshipTarget instanceof ConcreteEntityAdapter)) { - throw new Error("Expected concrete target"); + if (relationshipTarget instanceof InterfaceEntityAdapter) { + throw new Error("Unexpected interface target"); } const relationshipSource = relationshipAdapter.source; - if (relationshipSource instanceof UnionEntityAdapter) { - throw new Error("Unexpected union source"); - } const whereInput = composer.getITC(relationshipSource.operations.whereInputTypeName); const fields = augmentWhereInputTypeWithRelationshipFields(relationshipAdapter, deprecatedDirectives); whereInput.addFields(fields); + // TODO: Current unions are not supported as relationship targets beyond the above fields + if (relationshipTarget instanceof UnionEntityAdapter) { + return; + } + const whereAggregateInput = withAggregateInputType({ relationshipAdapter, entityAdapter: relationshipTarget, composer: composer, }); + if (relationshipAdapter.isFilterableByAggregate()) { whereInput.addFields({ [relationshipAdapter.operations.aggregateTypeName]: { diff --git a/packages/graphql/src/schema/get-nodes.ts b/packages/graphql/src/schema/get-nodes.ts index 735c3800170..fe6d95482e6 100644 --- a/packages/graphql/src/schema/get-nodes.ts +++ b/packages/graphql/src/schema/get-nodes.ts @@ -18,8 +18,7 @@ */ import type { IResolvers } from "@graphql-tools/utils"; -import type { DirectiveNode, NamedTypeNode } from "graphql"; -import type { Exclude } from "../classes"; +import type { NamedTypeNode } from "graphql"; import { Node } from "../classes"; import type { NodeDirective } from "../classes/NodeDirective"; import type { LimitDirective } from "../classes/LimitDirective"; @@ -27,12 +26,10 @@ import type { FullText, Neo4jGraphQLCallbacks } from "../types"; import { asArray } from "../utils/utils"; import type { DefinitionNodes } from "./get-definition-nodes"; import getObjFieldMeta from "./get-obj-field-meta"; -import parseExcludeDirective from "./parse-exclude-directive"; import parseNodeDirective from "./parse-node-directive"; import parseFulltextDirective from "./parse/parse-fulltext-directive"; import parsePluralDirective from "./parse/parse-plural-directive"; import { parseLimitDirective } from "./parse/parse-limit-directive"; -import { schemaConfigurationFromObjectTypeDefinition } from "./schema-configuration"; type Nodes = { nodes: Node[]; @@ -81,40 +78,12 @@ function getNodes( ["deprecated", "shareable"].includes(x.name.value) ); - const excludeDirective = (definition.directives || []).find((x) => x.name.value === "exclude"); const nodeDirectiveDefinition = (definition.directives || []).find((x) => x.name.value === "node"); const pluralDirectiveDefinition = (definition.directives || []).find((x) => x.name.value === "plural"); const fulltextDirectiveDefinition = (definition.directives || []).find((x) => x.name.value === "fulltext"); const limitDirectiveDefinition = (definition.directives || []).find((x) => x.name.value === "limit"); const nodeInterfaces = [...(definition.interfaces || [])] as NamedTypeNode[]; - const { interfaceExcludeDirectives } = nodeInterfaces.reduce<{ - interfaceAuthDirectives: DirectiveNode[]; - interfaceExcludeDirectives: DirectiveNode[]; - }>( - (res, interfaceName) => { - const iface = definitionNodes.interfaceTypes.find((i) => i.name.value === interfaceName.name.value); - - if (iface) { - const interfaceExcludeDirective = (iface.directives || []).find((x) => x.name.value === "exclude"); - - if (interfaceExcludeDirective) { - res.interfaceExcludeDirectives.push(interfaceExcludeDirective); - } - } - - return res; - }, - { interfaceAuthDirectives: [], interfaceExcludeDirectives: [] } - ); - - let exclude: Exclude; - if (excludeDirective || interfaceExcludeDirectives.length) { - exclude = parseExcludeDirective(excludeDirective || interfaceExcludeDirectives[0]); - } - - const schemaConfiguration = schemaConfigurationFromObjectTypeDefinition(definition); - let nodeDirective: NodeDirective; if (nodeDirectiveDefinition) { nodeDirective = parseNodeDirective(nodeDirectiveDefinition); @@ -181,9 +150,6 @@ function getNodes( propagatedDirectives, ...nodeFields, // @ts-ignore we can be sure it's defined - exclude, - schemaConfiguration, - // @ts-ignore we can be sure it's defined nodeDirective, // @ts-ignore we can be sure it's defined fulltextDirective, diff --git a/packages/graphql/src/schema/get-obj-field-meta.ts b/packages/graphql/src/schema/get-obj-field-meta.ts index 43167fc9816..3e11e35606b 100644 --- a/packages/graphql/src/schema/get-obj-field-meta.ts +++ b/packages/graphql/src/schema/get-obj-field-meta.ts @@ -170,8 +170,6 @@ function getObjFieldMeta({ "id", "authorization", "authentication", - "readonly", - "writeonly", "customResolver", "default", "coalesce", @@ -190,12 +188,6 @@ function getObjFieldMeta({ ), arguments: [...(field.arguments || [])], description: field.description?.value, - readonly: - directives.some((d) => d.name.value === "readonly") || - interfaceField?.directives?.some((x) => x.name.value === "readonly"), - writeonly: - directives.some((d) => d.name.value === "writeonly") || - interfaceField?.directives?.some((x) => x.name.value === "writeonly"), ...(unique ? { unique } : {}), }; diff --git a/packages/graphql/src/schema/make-augmented-schema.ts b/packages/graphql/src/schema/make-augmented-schema.ts index 7f89e058d74..734d101ddd1 100644 --- a/packages/graphql/src/schema/make-augmented-schema.ts +++ b/packages/graphql/src/schema/make-augmented-schema.ts @@ -229,6 +229,7 @@ function makeAugmentedSchema({ userDefinedDirectivesForInterface, userDefinedFieldDirectivesForNode, features, + experimental, }); seenRelationshipPropertiesInterfaces.add(relationshipAdapter.propertiesTypeName); } @@ -287,7 +288,13 @@ function makeAugmentedSchema({ propagatedDirectives, composer, }); - withWhereInputType({ entityAdapter: concreteEntityAdapter, userDefinedFieldDirectives, features, composer }); + withWhereInputType({ + entityAdapter: concreteEntityAdapter, + userDefinedFieldDirectives, + features, + composer, + experimental, + }); /** * TODO [translation-layer-compatibility] * Need to migrate resolvers, which themselves rely on the translation layer being migrated to the new schema model @@ -328,7 +335,6 @@ function makeAugmentedSchema({ if (concreteEntityAdapter.isReadable) { composer.Query.addFields({ [concreteEntityAdapter.operations.rootTypeFieldNames.read]: findResolver({ - node, entityAdapter: concreteEntityAdapter, }), }); @@ -338,7 +344,6 @@ function makeAugmentedSchema({ ); composer.Query.addFields({ [concreteEntityAdapter.operations.rootTypeFieldNames.connection]: rootConnectionResolver({ - node, composer, concreteEntityAdapter, propagatedDirectives, @@ -352,7 +357,6 @@ function makeAugmentedSchema({ if (concreteEntityAdapter.isAggregable) { composer.Query.addFields({ [concreteEntityAdapter.operations.rootTypeFieldNames.aggregate]: aggregateResolver({ - node, concreteEntityAdapter, }), }); @@ -412,6 +416,7 @@ function makeAugmentedSchema({ userDefinedFieldDirectives: new Map(), features, composer, + experimental, }); if (experimental) { // strip-out the schema config directives from the union type @@ -455,6 +460,7 @@ function makeAugmentedSchema({ userDefinedFieldDirectives, features, composer, + experimental, }); withOptionsInputType({ entityAdapter: interfaceEntityAdapter, userDefinedFieldDirectives, composer }); if (interfaceEntityAdapter.isReadable) { @@ -489,6 +495,7 @@ function makeAugmentedSchema({ schemaModel, userDefinedFieldDirectivesForNode, generateRelationshipTypes: !isCDCEngine, + experimental, }); } @@ -652,12 +659,14 @@ function doForRelationshipPropertiesInterface({ userDefinedDirectivesForInterface, userDefinedFieldDirectivesForNode, features, + experimental, }: { composer: SchemaComposer; relationshipAdapter: RelationshipAdapter; userDefinedDirectivesForInterface: Map; userDefinedFieldDirectivesForNode: Map>; features?: Neo4jFeaturesSettings; + experimental: boolean; }) { if (!relationshipAdapter.propertiesTypeName) { return; @@ -674,7 +683,13 @@ function doForRelationshipPropertiesInterface({ }); withSortInputType({ relationshipAdapter, userDefinedFieldDirectives, composer }); withUpdateInputType({ entityAdapter: relationshipAdapter, userDefinedFieldDirectives, composer }); - withWhereInputType({ entityAdapter: relationshipAdapter, userDefinedFieldDirectives, features, composer }); + withWhereInputType({ + entityAdapter: relationshipAdapter, + userDefinedFieldDirectives, + features, + composer, + experimental, + }); withCreateInputType({ entityAdapter: relationshipAdapter, userDefinedFieldDirectives, composer }); } @@ -706,7 +721,13 @@ function doForInterfacesThatAreTargetOfARelationship({ DirectiveNode[] >; withOptionsInputType({ entityAdapter: interfaceEntityAdapter, userDefinedFieldDirectives, composer }); - withWhereInputType({ entityAdapter: interfaceEntityAdapter, userDefinedFieldDirectives, features, composer }); + withWhereInputType({ + entityAdapter: interfaceEntityAdapter, + userDefinedFieldDirectives, + features, + composer, + experimental, + }); withCreateInputType({ entityAdapter: interfaceEntityAdapter, userDefinedFieldDirectives, composer }); withUpdateInputType({ entityAdapter: interfaceEntityAdapter, userDefinedFieldDirectives, composer }); diff --git a/packages/graphql/src/schema/parse-exclude-directive.test.ts b/packages/graphql/src/schema/parse-exclude-directive.test.ts deleted file mode 100644 index 3a8103bb9a4..00000000000 --- a/packages/graphql/src/schema/parse-exclude-directive.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DirectiveNode } from "graphql"; -import { parse } from "graphql"; -import parseExcludeDirective from "./parse-exclude-directive"; -import { Exclude } from "../classes"; - -describe("parseExcludeDirective", () => { - test("should throw an error if incorrect directive is passed in", () => { - const typeDefs = ` - type TestType @wrongdirective { - name: String - } - `; - - const directive = (parse(typeDefs) as any).definitions[0].directives[0] as DirectiveNode | undefined; - - expect(() => parseExcludeDirective(directive)).toThrow( - "Undefined or incorrect directive passed into parseExcludeDirective function" - ); - }); - - test("should return array of operations to ignore given valid array input", () => { - const typeDefs = ` - type TestType @exclude(operations: [CREATE, DELETE]) { - name: String - } - `; - - const directive = (parse(typeDefs) as any).definitions[0].directives[0] as DirectiveNode | undefined; - - const expected = new Exclude({ operations: ["create", "delete"] }); - - expect(parseExcludeDirective(directive)).toMatchObject(expected); - }); - - test("should return array of all operations to ignore given valid input of '*'", () => { - const typeDefs = ` - type TestType @exclude { - name: String - } - `; - - const directive = (parse(typeDefs) as any).definitions[0].directives[0] as DirectiveNode | undefined; - - const expected = new Exclude({ operations: ["create", "read", "update", "delete", "subscribe"] }); - - expect(parseExcludeDirective(directive)).toMatchObject(expected); - }); -}); diff --git a/packages/graphql/src/schema/parse-exclude-directive.ts b/packages/graphql/src/schema/parse-exclude-directive.ts deleted file mode 100644 index a5e6ed3df44..00000000000 --- a/packages/graphql/src/schema/parse-exclude-directive.ts +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { DirectiveNode, ArgumentNode } from "graphql"; -import { valueFromASTUntyped } from "graphql"; -import { Exclude } from "../classes"; - -function parseExcludeDirective(excludeDirective: DirectiveNode | undefined | null) { - if (!excludeDirective || excludeDirective.name.value !== "exclude") { - throw new Error("Undefined or incorrect directive passed into parseExcludeDirective function"); - } - - const allResolvers = ["create", "read", "update", "delete", "subscribe"]; - - if (!excludeDirective.arguments?.length) { - return new Exclude({ operations: allResolvers }); - } - - const operations = excludeDirective.arguments?.find((a) => a.name.value === "operations") as ArgumentNode; - - const argumentValue = valueFromASTUntyped(operations.value) as string[]; - - const result = argumentValue.map((val: string) => val.toLowerCase()); - - return new Exclude({ operations: result }); -} - -export default parseExcludeDirective; diff --git a/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts b/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts index 0bbc2416118..fcd806a3069 100644 --- a/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts +++ b/packages/graphql/src/schema/resolvers/composition/wrap-query-and-mutation.ts @@ -44,6 +44,7 @@ export type WrapResolverArguments = { dbInfo?: Neo4jDatabaseInfo; features: ContextFeatures; authorization?: Neo4jGraphQLAuthorization; + experimental: boolean; }; /** @@ -63,6 +64,7 @@ export interface Neo4jGraphQLComposedContext extends Neo4jGraphQLContext { subscriptionsEnabled: boolean; executor: Executor; authorization: AuthorizationContext; + experimental: boolean; neo4jDatabaseInfo?: Neo4jDatabaseInfo; fulltext?: FulltextContext; } @@ -79,6 +81,7 @@ export const wrapQueryAndMutation = dbInfo, authorization, features, + experimental, }: WrapResolverArguments) => (next: GraphQLFieldResolver) => async (root, args, context: Neo4jGraphQLContext, info: GraphQLResolveInfo) => { @@ -125,6 +128,7 @@ export const wrapQueryAndMutation = executor, neo4jDatabaseInfo, authorization: authorizationContext, + experimental, // Consider anything in here overrides ...context, }; diff --git a/packages/graphql/src/schema/resolvers/query/aggregate.ts b/packages/graphql/src/schema/resolvers/query/aggregate.ts index 1fde1c410b9..9fa70d0462d 100644 --- a/packages/graphql/src/schema/resolvers/query/aggregate.ts +++ b/packages/graphql/src/schema/resolvers/query/aggregate.ts @@ -18,7 +18,6 @@ */ import type { GraphQLResolveInfo } from "graphql"; -import type { Node } from "../../../classes"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { InterfaceEntityAdapter } from "../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; import { translateAggregate } from "../../../translate"; @@ -28,10 +27,8 @@ import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; export function aggregateResolver({ - node, concreteEntityAdapter, }: { - node?: Node; concreteEntityAdapter: ConcreteEntityAdapter | InterfaceEntityAdapter; }) { async function resolve(_root: any, _args: any, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) { @@ -41,7 +38,6 @@ export function aggregateResolver({ const { cypher, params } = translateAggregate({ context: context as Neo4jGraphQLTranslationContext, - node, entityAdapter: concreteEntityAdapter, }); diff --git a/packages/graphql/src/schema/resolvers/query/fulltext.ts b/packages/graphql/src/schema/resolvers/query/fulltext.ts index 8eeecedff32..e432e6e5440 100644 --- a/packages/graphql/src/schema/resolvers/query/fulltext.ts +++ b/packages/graphql/src/schema/resolvers/query/fulltext.ts @@ -57,7 +57,7 @@ export function fulltextResolver({ (context as Neo4jGraphQLTranslationContext).resolveTree = resolveTree; const { cypher, params } = translateRead( - { context: context as Neo4jGraphQLTranslationContext, node, entityAdapter }, + { context: context as Neo4jGraphQLTranslationContext, entityAdapter }, node.singular ); const executeResult = await execute({ diff --git a/packages/graphql/src/schema/resolvers/query/global-node.test.ts b/packages/graphql/src/schema/resolvers/query/global-node.test.ts index 938b442ef30..1195a268d45 100644 --- a/packages/graphql/src/schema/resolvers/query/global-node.test.ts +++ b/packages/graphql/src/schema/resolvers/query/global-node.test.ts @@ -18,17 +18,10 @@ */ import { globalNodeResolver } from "./global-node"; -import { NodeBuilder } from "../../../../tests/utils/builders/node-builder"; describe("Global node resolver", () => { test("should return the correct type, args and resolve", () => { - const node = new NodeBuilder({ - name: "Movie", - primitiveFields: [], - isGlobalNode: true, - }).instance(); - - const result = globalNodeResolver({ nodes: [node], entities: [] }); + const result = globalNodeResolver({ entities: [] }); expect(result.type).toBe("Node"); expect(result.resolve).toBeInstanceOf(Function); expect(result.args).toMatchObject({ diff --git a/packages/graphql/src/schema/resolvers/query/global-node.ts b/packages/graphql/src/schema/resolvers/query/global-node.ts index 520eb3d454a..12521db7787 100644 --- a/packages/graphql/src/schema/resolvers/query/global-node.ts +++ b/packages/graphql/src/schema/resolvers/query/global-node.ts @@ -20,16 +20,15 @@ import type { GraphQLResolveInfo } from "graphql"; import type { FieldsByTypeName } from "graphql-parse-resolve-info"; import { parseResolveInfo } from "graphql-parse-resolve-info"; -import { execute } from "../../../utils"; +import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { translateRead } from "../../../translate"; -import type { Node } from "../../../classes"; +import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; +import { execute } from "../../../utils"; +import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; import { fromGlobalId } from "../../../utils/global-ids"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; -import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; -import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; -import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -export function globalNodeResolver({ nodes, entities }: { nodes: Node[]; entities: ConcreteEntityAdapter[] }) { +export function globalNodeResolver({ entities }: { entities: ConcreteEntityAdapter[] }) { async function resolve( _root: any, args: { id: string }, @@ -40,7 +39,6 @@ export function globalNodeResolver({ nodes, entities }: { nodes: Node[]; entitie if (!typeName || !field || !id) return null; - const node = nodes.find((n) => n.name === typeName); const entityAdapter = entities.find((n) => n.name === typeName); if (!entityAdapter) return null; @@ -70,7 +68,6 @@ export function globalNodeResolver({ nodes, entities }: { nodes: Node[]; entitie const { cypher, params } = translateRead({ context: context as Neo4jGraphQLTranslationContext, - node, entityAdapter, }); const executeResult = await execute({ diff --git a/packages/graphql/src/schema/resolvers/query/read.test.ts b/packages/graphql/src/schema/resolvers/query/read.test.ts index 14d8788a605..4722e3e6f6c 100644 --- a/packages/graphql/src/schema/resolvers/query/read.test.ts +++ b/packages/graphql/src/schema/resolvers/query/read.test.ts @@ -17,16 +17,12 @@ * limitations under the License. */ -import { NodeBuilder } from "../../../../tests/utils/builders/node-builder"; import { ConcreteEntity } from "../../../schema-model/entity/ConcreteEntity"; import { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { findResolver } from "./read"; describe("Read resolver", () => { test("should return the correct; type, args and resolve", () => { - const node = new NodeBuilder({ - name: "Movie", - }).instance(); const concreteEntity = new ConcreteEntity({ name: "Movie", labels: ["Movie"], @@ -38,7 +34,7 @@ describe("Read resolver", () => { }); const concreteEntityAdapter = new ConcreteEntityAdapter(concreteEntity); - const result = findResolver({ node, entityAdapter: concreteEntityAdapter }); + const result = findResolver({ entityAdapter: concreteEntityAdapter }); expect(result.type).toBe(`[Movie!]!`); expect(result.resolve).toBeInstanceOf(Function); expect(result.args).toMatchObject({ diff --git a/packages/graphql/src/schema/resolvers/query/read.ts b/packages/graphql/src/schema/resolvers/query/read.ts index f4a9c8df0c9..75bbb13e0a2 100644 --- a/packages/graphql/src/schema/resolvers/query/read.ts +++ b/packages/graphql/src/schema/resolvers/query/read.ts @@ -18,7 +18,6 @@ */ import type { GraphQLResolveInfo } from "graphql"; -import type { Node } from "../../../classes"; import { QueryOptions } from "../../../graphql/input-objects/QueryOptions"; import type { EntityAdapter } from "../../../schema-model/entity/EntityAdapter"; import { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; @@ -29,7 +28,7 @@ import { execute } from "../../../utils"; import getNeo4jResolveTree from "../../../utils/get-neo4j-resolve-tree"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; -export function findResolver({ node, entityAdapter }: { node?: Node; entityAdapter: EntityAdapter }) { +export function findResolver({ entityAdapter }: { entityAdapter: EntityAdapter }) { async function resolve(_root: any, args: any, context: Neo4jGraphQLComposedContext, info: GraphQLResolveInfo) { const resolveTree = getNeo4jResolveTree(info, { args }); @@ -37,7 +36,6 @@ export function findResolver({ node, entityAdapter }: { node?: Node; entityAdapt const { cypher, params } = translateRead({ context: context as Neo4jGraphQLTranslationContext, - node, entityAdapter, }); const executeResult = await execute({ diff --git a/packages/graphql/src/schema/resolvers/query/root-connection.ts b/packages/graphql/src/schema/resolvers/query/root-connection.ts index 46f46d4ca55..83cccd9206b 100644 --- a/packages/graphql/src/schema/resolvers/query/root-connection.ts +++ b/packages/graphql/src/schema/resolvers/query/root-connection.ts @@ -27,7 +27,6 @@ import { } from "graphql"; import type { InputTypeComposer, SchemaComposer } from "graphql-compose"; import type { PageInfo as PageInfoRelay } from "graphql-relay"; -import type { Node } from "../../../classes"; import { PageInfo } from "../../../graphql/objects/PageInfo"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import { translateRead } from "../../../translate"; @@ -40,12 +39,10 @@ import { graphqlDirectivesToCompose } from "../../to-compose"; import type { Neo4jGraphQLComposedContext } from "../composition/wrap-query-and-mutation"; export function rootConnectionResolver({ - node, composer, concreteEntityAdapter, propagatedDirectives, }: { - node: Node; composer: SchemaComposer; concreteEntityAdapter: ConcreteEntityAdapter; propagatedDirectives: DirectiveNode[]; @@ -56,9 +53,7 @@ export function rootConnectionResolver({ const { cypher, params } = translateRead({ context: context as Neo4jGraphQLTranslationContext, - node, entityAdapter: concreteEntityAdapter, - isRootConnectionField: true, }); const executeResult = await execute({ diff --git a/packages/graphql/src/schema/schema-configuration.test.ts b/packages/graphql/src/schema/schema-configuration.test.ts deleted file mode 100644 index c3be009048b..00000000000 --- a/packages/graphql/src/schema/schema-configuration.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ObjectTypeDefinitionNode, SchemaExtensionNode } from "graphql"; -import { Kind, parse } from "graphql"; -import { QueryDirective } from "../classes/QueryDirective"; -import { SubscriptionDirective } from "../classes/SubscriptionDirective"; -import { - getSchemaConfigurationFlags, - schemaConfigurationFromObjectTypeDefinition, - schemaConfigurationFromSchemaExtensions, -} from "./schema-configuration"; -import parseExcludeDirective from "./parse-exclude-directive"; -import { SubscriptionEvent } from "../graphql/directives/subscription"; - -describe("schemaConfiguration", () => { - test("schemaConfigurationFromObjectTypeDefinition should return a Schema Configuration object", () => { - const typeDefs = ` - type TestType @query(read:false) @subscription { - name: String - } - `; - - const definition = parse(typeDefs).definitions[0] as ObjectTypeDefinitionNode; - const schemaConfiguration = schemaConfigurationFromObjectTypeDefinition(definition); - - expect(schemaConfiguration).toMatchObject({ - queryDirective: new QueryDirective({ read: false, aggregate: false }), - subscriptionDirective: new SubscriptionDirective([ - SubscriptionEvent.CREATED, - SubscriptionEvent.DELETED, - SubscriptionEvent.UPDATED, - SubscriptionEvent.RELATIONSHIP_CREATED, - SubscriptionEvent.RELATIONSHIP_DELETED, - ]), - }); - }); - - test("schemaConfigurationFromSchemaExtensions should return a SchemaConfiguration object", () => { - const typeDefs = ` - type TestType @query(read:false) { - name: String - } - extend schema @subscription - `; - - const schemaExtensions = parse(typeDefs).definitions.filter( - (definition) => definition.kind === Kind.SCHEMA_EXTENSION - ) as SchemaExtensionNode[]; - const schemaConfiguration = schemaConfigurationFromSchemaExtensions(schemaExtensions); - - expect(schemaConfiguration).toMatchObject({ - subscriptionDirective: new SubscriptionDirective([ - SubscriptionEvent.CREATED, - SubscriptionEvent.DELETED, - SubscriptionEvent.UPDATED, - SubscriptionEvent.RELATIONSHIP_CREATED, - SubscriptionEvent.RELATIONSHIP_DELETED, - ]), - }); - }); - - test("getSchemaConfigurationFlags should return the correct flags", () => { - const typeDefs = ` - type TestType @query(read:false) @mutation(operations: [CREATE, DELETE]) { - name: String - } - extend schema @subscription(events: [CREATED]) - `; - - const documentNode = parse(typeDefs); - const schemaExtensions = documentNode.definitions.filter( - (definition) => definition.kind === Kind.SCHEMA_EXTENSION - ) as SchemaExtensionNode[]; - const objectTypeDefinition = documentNode.definitions.find( - (definition) => definition.kind === Kind.OBJECT_TYPE_DEFINITION - ) as ObjectTypeDefinitionNode; - const nodeSchemaConfiguration = schemaConfigurationFromObjectTypeDefinition(objectTypeDefinition); - const globalSchemaConfiguration = schemaConfigurationFromSchemaExtensions(schemaExtensions); - const schemaConfigurationFlags = getSchemaConfigurationFlags({ - nodeSchemaConfiguration, - globalSchemaConfiguration, - }); - - expect(schemaConfigurationFlags).toEqual({ - read: false, - aggregate: false, - create: true, - delete: true, - update: false, - subscribeCreate: true, - subscribeCreateRelationship: false, - subscribeDelete: false, - subscribeDeleteRelationship: false, - subscribeUpdate: false, - }); - }); - - test("getSchemaConfigurationFlags should return the correct flags when used with the deprecated exclude directive", () => { - const typeDefs = ` - type TestType @exclude(operations: [CREATE, DELETE]) { - name: String - } - `; - - const documentNode = parse(typeDefs); - const objectTypeDefinition = documentNode.definitions.find( - (definition) => definition.kind === Kind.OBJECT_TYPE_DEFINITION - ) as ObjectTypeDefinitionNode; - const exclude = (objectTypeDefinition.directives as any[])[0]; - const excludeDirective = parseExcludeDirective(exclude); - const schemaConfigurationFlags = getSchemaConfigurationFlags({ - excludeDirective, - }); - - expect(schemaConfigurationFlags).toEqual({ - read: true, - aggregate: true, - create: false, - delete: false, - update: true, - subscribeCreate: true, - subscribeCreateRelationship: true, - subscribeDelete: true, - subscribeDeleteRelationship: true, - subscribeUpdate: true, - }); - }); -}); diff --git a/packages/graphql/src/schema/schema-configuration.ts b/packages/graphql/src/schema/schema-configuration.ts deleted file mode 100644 index 9406ba9d082..00000000000 --- a/packages/graphql/src/schema/schema-configuration.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright (c) "Neo4j" - * Neo4j Sweden AB [http://neo4j.com] - * - * This file is part of Neo4j. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import type { ObjectTypeDefinitionNode, SchemaExtensionNode } from "graphql"; -import type { Exclude } from "../classes"; -import type { MutationDirective } from "../classes/MutationDirective"; -import type { QueryDirective } from "../classes/QueryDirective"; -import type { SubscriptionDirective } from "../classes/SubscriptionDirective"; -import { - mutationDirective as mutationDirectiveDefinition, - queryDirective as queryDirectiveDefinition, - subscriptionDirective as subscriptionDirectiveDefinition, -} from "../graphql/directives"; -import parseMutationDirective from "./parse-mutation-directive"; -import parseQueryDirective from "./parse-query-directive"; -import parseSubscriptionDirective from "./parse-subscription-directive"; - -export type SchemaConfiguration = { - queryDirective?: QueryDirective; - mutationDirective?: MutationDirective; - subscriptionDirective?: SubscriptionDirective; -}; - -type SchemaConfigurationFlags = { - read: boolean; - aggregate: boolean; - create: boolean; - delete: boolean; - update: boolean; - subscribeCreate: boolean; - subscribeUpdate: boolean; - subscribeDelete: boolean; - subscribeCreateRelationship: boolean; - subscribeDeleteRelationship: boolean; -}; - -// obtain a schema configuration object from a list of SchemaExtensionNode -export function schemaConfigurationFromSchemaExtensions(schemaExtensions: SchemaExtensionNode[]): SchemaConfiguration { - const schemaConfiguration: SchemaConfiguration = {}; - - for (const schemaExtension of schemaExtensions) { - for (const directive of schemaExtension.directives || []) { - if (directive.name.value === queryDirectiveDefinition.name) { - if (schemaConfiguration.queryDirective) { - throw new Error(`Ambiguous usage with the directive named: ${queryDirectiveDefinition.name}`); - } - schemaConfiguration.queryDirective = parseQueryDirective(directive); - } - - if (directive.name.value === mutationDirectiveDefinition.name) { - if (schemaConfiguration.mutationDirective) { - throw new Error(`Ambiguity usage with the directive named: ${queryDirectiveDefinition.name}`); - } - schemaConfiguration.mutationDirective = parseMutationDirective(directive); - } - - if (directive.name.value === subscriptionDirectiveDefinition.name) { - if (schemaConfiguration.subscriptionDirective) { - throw new Error(`Ambiguous usage with the directive named: ${queryDirectiveDefinition.name}`); - } - schemaConfiguration.subscriptionDirective = parseSubscriptionDirective(directive); - } - } - } - return schemaConfiguration; -} - -// obtain a schema configuration object from a ObjectTypeDefinition -export function schemaConfigurationFromObjectTypeDefinition(definition: ObjectTypeDefinitionNode): SchemaConfiguration { - const schemaConfiguration: SchemaConfiguration = {}; - - for (const directive of definition.directives || []) { - if (directive.name.value === queryDirectiveDefinition.name) { - schemaConfiguration.queryDirective = parseQueryDirective(directive); - } - - if (directive.name.value === mutationDirectiveDefinition.name) { - schemaConfiguration.mutationDirective = parseMutationDirective(directive); - } - - if (directive.name.value === subscriptionDirectiveDefinition.name) { - schemaConfiguration.subscriptionDirective = parseSubscriptionDirective(directive); - } - } - - return schemaConfiguration; -} - -// takes the directives that may mutate the output schema and returns a SchemaConfigurationFlags object -export function getSchemaConfigurationFlags(options: { - nodeSchemaConfiguration?: SchemaConfiguration; - globalSchemaConfiguration?: SchemaConfiguration; - excludeDirective?: Exclude; -}) { - // avoid mixing between the exclude directive and the new schema configurations one - if ( - options.excludeDirective && - (options.globalSchemaConfiguration?.queryDirective || - options.nodeSchemaConfiguration?.queryDirective || - options.globalSchemaConfiguration?.mutationDirective || - options.nodeSchemaConfiguration?.mutationDirective) - ) { - throw new Error( - "@exclude directive is a deprecated directive and cannot be used in conjunction with @query, @mutation, @subscription" - ); - } - - // avoid mixing configurations on both schema and object - if (options.globalSchemaConfiguration?.queryDirective && options.nodeSchemaConfiguration?.queryDirective) { - throw new Error("@query directive already defined at the schema location"); - } - - if (options.globalSchemaConfiguration?.mutationDirective && options.nodeSchemaConfiguration?.mutationDirective) { - throw new Error("@mutation directive already defined at the schema location"); - } - - if ( - options.globalSchemaConfiguration?.subscriptionDirective && - options.nodeSchemaConfiguration?.subscriptionDirective - ) { - throw new Error("@subscription directive already defined at the schema location"); - } - - const schemaConfigurationFlags: SchemaConfigurationFlags = { - read: true, - aggregate: true, - create: true, - delete: true, - update: true, - subscribeCreate: true, - subscribeUpdate: true, - subscribeDelete: true, - subscribeCreateRelationship: true, - subscribeDeleteRelationship: true, - }; - - if (options.excludeDirective) { - const excludeOperationsSet = new Set(options.excludeDirective.operations); - schemaConfigurationFlags.read = schemaConfigurationFlags.aggregate = !excludeOperationsSet.has("read"); - schemaConfigurationFlags.create = !excludeOperationsSet.has("create"); - schemaConfigurationFlags.delete = !excludeOperationsSet.has("delete"); - schemaConfigurationFlags.update = !excludeOperationsSet.has("update"); - } - - const queryDirective = - options.nodeSchemaConfiguration?.queryDirective || options.globalSchemaConfiguration?.queryDirective; - const mutationDirective = - options.nodeSchemaConfiguration?.mutationDirective || options.globalSchemaConfiguration?.mutationDirective; - const subscriptionDirective = - options.globalSchemaConfiguration?.subscriptionDirective || - options.nodeSchemaConfiguration?.subscriptionDirective; - - if (queryDirective) { - schemaConfigurationFlags.read = queryDirective.read; - schemaConfigurationFlags.aggregate = queryDirective.aggregate; - } - - if (mutationDirective) { - schemaConfigurationFlags.create = mutationDirective.create; - schemaConfigurationFlags.update = mutationDirective.update; - schemaConfigurationFlags.delete = mutationDirective.delete; - } - - if (subscriptionDirective) { - schemaConfigurationFlags.subscribeCreate = subscriptionDirective.created; - schemaConfigurationFlags.subscribeUpdate = subscriptionDirective.updated; - schemaConfigurationFlags.subscribeDelete = subscriptionDirective.deleted; - schemaConfigurationFlags.subscribeCreateRelationship = subscriptionDirective.relationshipCreated; - schemaConfigurationFlags.subscribeDeleteRelationship = subscriptionDirective.relationshipDeleted; - } - - return schemaConfigurationFlags; -} diff --git a/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts b/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts index 777248f8f75..6ad495618f4 100644 --- a/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts +++ b/packages/graphql/src/schema/subscriptions/generate-subscription-types.ts @@ -38,11 +38,13 @@ export function generateSubscriptionTypes({ schemaModel, userDefinedFieldDirectivesForNode, generateRelationshipTypes, + experimental, }: { schemaComposer: SchemaComposer; schemaModel: Neo4jGraphQLSchemaModel; userDefinedFieldDirectivesForNode: Map>; generateRelationshipTypes: boolean; + experimental: boolean; }): void { const subscriptionComposer = schemaComposer.Subscription; @@ -301,6 +303,7 @@ export function generateSubscriptionTypes({ const connectionWhere = generateSubscriptionConnectionWhereType({ entityAdapter, schemaComposer, + experimental, }); if (entityAdapter.relationships.size > 0) { if (entityAdapter.isSubscribableOnRelationshipCreate) { diff --git a/packages/graphql/src/schema/subscriptions/generate-subscription-where-type.ts b/packages/graphql/src/schema/subscriptions/generate-subscription-where-type.ts index 39beb6d886a..524adec0d6c 100644 --- a/packages/graphql/src/schema/subscriptions/generate-subscription-where-type.ts +++ b/packages/graphql/src/schema/subscriptions/generate-subscription-where-type.ts @@ -46,13 +46,16 @@ export function generateSubscriptionWhereType( export function generateSubscriptionConnectionWhereType({ entityAdapter, schemaComposer, + experimental, }: { entityAdapter: ConcreteEntityAdapter; schemaComposer: SchemaComposer; + experimental: boolean; }): { created: InputTypeComposer; deleted: InputTypeComposer } | undefined { const connectedRelationship = getRelationshipConnectionWhereTypes({ entityAdapter, schemaComposer, + experimental, }); const isConnectedNodeTypeNotExcluded = schemaComposer.has(entityAdapter.operations.subscriptionWhereInputTypeName); if (!isConnectedNodeTypeNotExcluded && !connectedRelationship) { @@ -91,15 +94,18 @@ export function generateSubscriptionConnectionWhereType({ function getRelationshipConnectionWhereTypes({ entityAdapter, schemaComposer, + experimental, }: { entityAdapter: ConcreteEntityAdapter; schemaComposer: SchemaComposer; + experimental: boolean; }): InputTypeComposer | undefined { const relationsFieldInputWhereTypeFields = Array.from(entityAdapter.relationships.values()).reduce( (acc, relationshipAdapter) => { const fields = makeNodeRelationFields({ relationshipAdapter, schemaComposer, + experimental, }); if (!fields) { return acc; @@ -127,9 +133,11 @@ function getRelationshipConnectionWhereTypes({ function makeNodeRelationFields({ relationshipAdapter, schemaComposer, + experimental, }: { relationshipAdapter: RelationshipAdapter; schemaComposer: SchemaComposer; + experimental: boolean; }) { const edgeType = makeRelationshipWhereType({ schemaComposer, @@ -148,6 +156,7 @@ function makeNodeRelationFields({ schemaComposer, interfaceEntity, edgeType, + experimental, }); } return makeRelationshipToConcreteTypeWhereType({ relationshipAdapter, edgeType, schemaComposer }); @@ -229,10 +238,12 @@ function makeRelationshipToInterfaceTypeWhereType({ schemaComposer, interfaceEntity, edgeType, + experimental, }: { schemaComposer: SchemaComposer; interfaceEntity: InterfaceEntityAdapter; edgeType: InputTypeComposer | undefined; + experimental: boolean; }): { node?: InputTypeComposer; edge?: InputTypeComposer } | undefined { let interfaceImplementationsType: InputTypeComposer | undefined = undefined; let interfaceNodeType: InputTypeComposer | undefined = undefined; @@ -251,7 +262,7 @@ function makeRelationshipToInterfaceTypeWhereType({ } const interfaceFields: InputTypeComposerFieldConfigMapDefinition = attributesToSubscriptionsWhereInputFields(interfaceEntity); - if (interfaceImplementationsType) { + if (interfaceImplementationsType && !experimental) { interfaceFields["_on"] = interfaceImplementationsType; } if (!isEmptyObject(interfaceFields)) { diff --git a/packages/graphql/src/schema/to-compose.ts b/packages/graphql/src/schema/to-compose.ts index 3fc49586334..8b6ad6b456d 100644 --- a/packages/graphql/src/schema/to-compose.ts +++ b/packages/graphql/src/schema/to-compose.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type { DirectiveNode, InputValueDefinitionNode } from "graphql"; +import type { DirectiveNode } from "graphql"; import { GraphQLInt } from "graphql"; import type { Directive, @@ -33,27 +33,11 @@ import type { ConcreteEntityAdapter } from "../schema-model/entity/model-adapter import type { InterfaceEntityAdapter } from "../schema-model/entity/model-adapters/InterfaceEntityAdapter"; import { parseValueNode } from "../schema-model/parser/parse-value-node"; import { RelationshipAdapter } from "../schema-model/relationship/model-adapters/RelationshipAdapter"; -import type { BaseField, InputField, PrimitiveField, TemporalField } from "../types"; +import type { InputField } from "../types"; import { DEPRECATE_NOT } from "./constants"; -import getFieldTypeMeta from "./get-field-type-meta"; import { idResolver } from "./resolvers/field/id"; import { numericalResolver } from "./resolvers/field/numerical"; -export function graphqlInputValueToCompose(args: InputValueDefinitionNode[]) { - return args.reduce((res, arg) => { - const meta = getFieldTypeMeta(arg.type); - - return { - ...res, - [arg.name.value]: { - type: meta.pretty, - description: arg.description, - ...(arg.defaultValue ? { defaultValue: parseValueNode(arg.defaultValue) } : {}), - }, - }; - }, {}); -} - export function graphqlArgsToCompose(args: Argument[]) { return args.reduce((res, arg) => { const inputValueAdapter = new ArgumentAdapter(arg); @@ -81,40 +65,6 @@ export function graphqlDirectivesToCompose(directives: DirectiveNode[]): Directi })); } -export function objectFieldsToComposeFields(fields: BaseField[]): { - [k: string]: ObjectTypeComposerFieldConfigAsObjectDefinition; -} { - return fields.reduce((res, field) => { - if (field.writeonly || field.selectableOptions.onRead === false) { - return res; - } - - const newField: ObjectTypeComposerFieldConfigAsObjectDefinition = { - type: field.typeMeta.pretty, - args: {}, - description: field.description, - }; - - if (field.otherDirectives.length) { - newField.directives = graphqlDirectivesToCompose(field.otherDirectives); - } - - if (["Int", "Float"].includes(field.typeMeta.name)) { - newField.resolve = numericalResolver; - } - - if (field.typeMeta.name === "ID") { - newField.resolve = idResolver; - } - - if (field.arguments) { - newField.args = graphqlInputValueToCompose(field.arguments); - } - - return { ...res, [field.fieldName]: newField }; - }, {}); -} - export function relationshipAdapterToComposeFields( objectFields: RelationshipAdapter[], userDefinedFieldDirectives: Map @@ -334,6 +284,7 @@ export function withMathOperators(): AdditionalFieldsCallback { return fields; }; } + export function withArrayOperators(): AdditionalFieldsCallback { return (attribute: AttributeAdapter): InputTypeComposerFieldConfigMapDefinition => { const fields: InputTypeComposerFieldConfigMapDefinition = {}; diff --git a/packages/graphql/src/translate/authorization/compatibility/compile-predicate-return.ts b/packages/graphql/src/translate/authorization/compatibility/compile-predicate-return.ts index dfac8b304fa..14f67bae1e5 100644 --- a/packages/graphql/src/translate/authorization/compatibility/compile-predicate-return.ts +++ b/packages/graphql/src/translate/authorization/compatibility/compile-predicate-return.ts @@ -44,7 +44,7 @@ export function compilePredicateReturn( let subqueries: string | undefined; if (predicate) { - const predicateCypher = new Cypher.RawCypher((env) => { + const predicateCypher = new Cypher.Raw((env) => { const predicateStr = compileCypher(predicate, env); if (preComputedSubqueries && !preComputedSubqueries.empty) { // Assign the Cypher string to a variable outside of the scope of the compilation diff --git a/packages/graphql/src/translate/authorization/create-authorization-after-predicate.ts b/packages/graphql/src/translate/authorization/create-authorization-after-predicate.ts index ea41cd89b0f..5c8e6f55d7d 100644 --- a/packages/graphql/src/translate/authorization/create-authorization-after-predicate.ts +++ b/packages/graphql/src/translate/authorization/create-authorization-after-predicate.ts @@ -49,7 +49,7 @@ export function createAuthorizationAfterPredicate({ if (!isConcreteEntity(entity)) { throw new Error("Expected authorization rule to be applied on a concrete entity"); } - const factory = new QueryASTFactory(context.schemaModel); + const factory = new QueryASTFactory(context.schemaModel, context.experimental); const queryASTEnv = new QueryASTEnv(); const queryASTContext = new QueryASTContext({ @@ -109,7 +109,7 @@ export function createAuthorizationAfterPredicateField({ if (!isConcreteEntity(entity)) { throw new Error("Expected authorization rule to be applied on a concrete entity"); } - const factory = new QueryASTFactory(context.schemaModel); + const factory = new QueryASTFactory(context.schemaModel, context.experimental); const queryASTEnv = new QueryASTEnv(); const queryASTContext = new QueryASTContext({ diff --git a/packages/graphql/src/translate/authorization/create-authorization-before-predicate.ts b/packages/graphql/src/translate/authorization/create-authorization-before-predicate.ts index 1ccd616b17a..a0cb42b85b8 100644 --- a/packages/graphql/src/translate/authorization/create-authorization-before-predicate.ts +++ b/packages/graphql/src/translate/authorization/create-authorization-before-predicate.ts @@ -49,7 +49,7 @@ export function createAuthorizationBeforePredicate({ if (!isConcreteEntity(entity)) { throw new Error("Expected authorization rule to be applied on a concrete entity"); } - const factory = new QueryASTFactory(context.schemaModel); + const factory = new QueryASTFactory(context.schemaModel, context.experimental); const queryASTEnv = new QueryASTEnv(); const queryASTContext = new QueryASTContext({ @@ -111,7 +111,7 @@ export function createAuthorizationBeforePredicateField({ if (!isConcreteEntity(entity)) { throw new Error("Expected authorization rule to be applied on a concrete entity"); } - const factory = new QueryASTFactory(context.schemaModel); + const factory = new QueryASTFactory(context.schemaModel, context.experimental); const queryASTEnv = new QueryASTEnv(); const queryASTContext = new QueryASTContext({ diff --git a/packages/graphql/src/translate/authorization/utils/populate-where-params.ts b/packages/graphql/src/translate/authorization/utils/populate-where-params.ts index 994ebe2f97c..434cbfacd48 100644 --- a/packages/graphql/src/translate/authorization/utils/populate-where-params.ts +++ b/packages/graphql/src/translate/authorization/utils/populate-where-params.ts @@ -32,11 +32,11 @@ export function populateWhereParams({ const parsed: GraphQLWhereArg = {}; Object.entries(where).forEach(([k, v]) => { - if (Array.isArray(v)) { + if (k === "AND" || k === "OR") { parsed[k] = v.map((w) => populateWhereParams({ where: w, context })); } else if (v === null) { parsed[k] = v; - } else if (typeof v === "object") { + } else if (typeof v === "object" && !Array.isArray(v)) { parsed[k] = populateWhereParams({ where: v, context }); } else if (typeof v === "string") { parsed[k] = parseContextParamProperty(v, context); diff --git a/packages/graphql/src/translate/batch-create/GraphQLInputAST/CreateAST.ts b/packages/graphql/src/translate/batch-create/GraphQLInputAST/CreateAST.ts index fd1829f564e..672fb01a4a5 100644 --- a/packages/graphql/src/translate/batch-create/GraphQLInputAST/CreateAST.ts +++ b/packages/graphql/src/translate/batch-create/GraphQLInputAST/CreateAST.ts @@ -17,21 +17,21 @@ * limitations under the License. */ -import type { Visitor, ICreateAST } from "./types"; +import type { Visitor } from "./types"; import type { Node } from "../../../classes"; -import { AST } from "./AST"; +import { UnwindASTNode } from "./UnwindASTNode"; -export class CreateAST extends AST implements ICreateAST { +export class CreateAST extends UnwindASTNode { nodeProperties: string[]; node: Node; - constructor(nodeProperties: string[], node: Node) { - super(); + constructor(id: number, nodeProperties: string[], node: Node) { + super(id); this.nodeProperties = nodeProperties; this.node = node; } - accept(visitor: Visitor): void { - visitor.visitCreate(this); + accept(visitor: Visitor): T { + return visitor.visitCreate(this); } } diff --git a/packages/graphql/src/translate/batch-create/GraphQLInputAST/GraphQLInputAST.ts b/packages/graphql/src/translate/batch-create/GraphQLInputAST/GraphQLInputAST.ts index b697050283c..38cc847d7a3 100644 --- a/packages/graphql/src/translate/batch-create/GraphQLInputAST/GraphQLInputAST.ts +++ b/packages/graphql/src/translate/batch-create/GraphQLInputAST/GraphQLInputAST.ts @@ -17,9 +17,9 @@ * limitations under the License. */ -import { AST } from "./AST"; +import { UnwindASTNode } from "./UnwindASTNode"; import { CreateAST } from "./CreateAST"; import { NestedCreateAST } from "./NestedCreateAST"; -export { AST, CreateAST, NestedCreateAST }; -export type { IAST, IConnectAST, IConnectOrCreateAST, ICreateAST, INestedCreateAST, Visitor } from "./types"; +export { UnwindASTNode, CreateAST, NestedCreateAST }; +export type { Visitor } from "./types"; diff --git a/packages/graphql/src/translate/batch-create/GraphQLInputAST/NestedCreateAST.ts b/packages/graphql/src/translate/batch-create/GraphQLInputAST/NestedCreateAST.ts index f4a2c6bf0fb..a664b0334b8 100644 --- a/packages/graphql/src/translate/batch-create/GraphQLInputAST/NestedCreateAST.ts +++ b/packages/graphql/src/translate/batch-create/GraphQLInputAST/NestedCreateAST.ts @@ -17,12 +17,11 @@ * limitations under the License. */ -import type { RelationField } from "../../../types"; -import type { Visitor, INestedCreateAST } from "./types"; +import type { Visitor } from "./types"; +import { UnwindASTNode } from "./UnwindASTNode"; import type { Node, Relationship } from "../../../classes"; -import { AST } from "./AST"; - -export class NestedCreateAST extends AST implements INestedCreateAST { +import type { RelationField } from "../../../types"; +export class NestedCreateAST extends UnwindASTNode { node: Node; parent: Node; nodeProperties: string[]; @@ -32,6 +31,7 @@ export class NestedCreateAST extends AST implements INestedCreateAST { edge: Relationship | undefined; constructor( + id: number, node: Node, parent: Node, nodeProperties: string[], @@ -40,7 +40,7 @@ export class NestedCreateAST extends AST implements INestedCreateAST { relationship: [RelationField | undefined, Node[]], edge?: Relationship ) { - super(); + super(id); this.node = node; this.parent = parent; this.nodeProperties = nodeProperties; @@ -50,7 +50,7 @@ export class NestedCreateAST extends AST implements INestedCreateAST { this.edge = edge; } - accept(visitor: Visitor): void { - visitor.visitNestedCreate(this); + accept(visitor: Visitor): T { + return visitor.visitNestedCreate(this); } } diff --git a/packages/graphql/src/translate/batch-create/GraphQLInputAST/AST.ts b/packages/graphql/src/translate/batch-create/GraphQLInputAST/UnwindASTNode.ts similarity index 70% rename from packages/graphql/src/translate/batch-create/GraphQLInputAST/AST.ts rename to packages/graphql/src/translate/batch-create/GraphQLInputAST/UnwindASTNode.ts index a40b3786360..2011428398c 100644 --- a/packages/graphql/src/translate/batch-create/GraphQLInputAST/AST.ts +++ b/packages/graphql/src/translate/batch-create/GraphQLInputAST/UnwindASTNode.ts @@ -17,16 +17,18 @@ * limitations under the License. */ -import type { IAST, Visitor } from "./types"; -import { v4 as uuidv4 } from "uuid"; +import type { Visitor } from "./types"; -export abstract class AST implements IAST { - id = uuidv4(); - children: IAST[] = []; +export abstract class UnwindASTNode { + public id: number; + public children: UnwindASTNode[] = []; - addChildren(node: IAST): void { + constructor(id: number) { + this.id = id; + } + addChildren(node: UnwindASTNode): void { this.children.push(node); } - abstract accept(visitor: Visitor): void; + abstract accept(visitor: Visitor): T; } diff --git a/packages/graphql/src/translate/batch-create/GraphQLInputAST/types.ts b/packages/graphql/src/translate/batch-create/GraphQLInputAST/types.ts index bcae7a6525d..cf2f2619bbc 100644 --- a/packages/graphql/src/translate/batch-create/GraphQLInputAST/types.ts +++ b/packages/graphql/src/translate/batch-create/GraphQLInputAST/types.ts @@ -17,51 +17,10 @@ * limitations under the License. */ -import type { TreeDescriptor } from "../types"; -import type { RelationField } from "../../../types"; -import type { Node, Relationship } from "../../../classes"; +import type { CreateAST } from "./CreateAST"; +import type { NestedCreateAST } from "./NestedCreateAST"; -export interface IAST { - id: string; - children: IAST[]; - addChildren: (children: IAST) => void; - accept: (visitor: Visitor) => void; -} - -export interface IConnectAST extends IAST { - node: Node; - parent: Node; - edgeProperties: string[]; - where: TreeDescriptor; - connect: TreeDescriptor; - relationshipPropertyPath: string; - relationship: [RelationField | undefined, Node[]]; -} - -export interface IConnectOrCreateAST extends IAST { - parent: Node; - where: TreeDescriptor; - onCreate: TreeDescriptor; -} - -export interface ICreateAST extends IAST { - nodeProperties: string[]; - node: Node; -} - -export interface INestedCreateAST extends IAST { - node: Node; - parent: Node; - nodeProperties: string[]; - edgeProperties: string[]; - relationshipPropertyPath: string; - relationship: [RelationField | undefined, Node[]]; - edge: Relationship | undefined; -} - -export interface Visitor { - visitCreate: (create: ICreateAST) => void; - visitNestedCreate: (nestedCreate: INestedCreateAST) => void; - // visitConnect: (connect: IConnectAST) => void; - // visitConnectOrCreate: (connectOrCreate: IConnectOrCreateAST) => void; +export interface Visitor { + visitCreate: (create: CreateAST) => T; + visitNestedCreate: (nestedCreate: NestedCreateAST) => T; } diff --git a/packages/graphql/src/translate/batch-create/parser.ts b/packages/graphql/src/translate/batch-create/parser.ts index 5042a5f12ea..011170870ca 100644 --- a/packages/graphql/src/translate/batch-create/parser.ts +++ b/packages/graphql/src/translate/batch-create/parser.ts @@ -23,7 +23,7 @@ import { UnsupportedUnwindOptimization } from "./types"; import type { GraphElement, Node, Relationship } from "../../classes"; import { Neo4jGraphQLError } from "../../classes"; import Cypher from "@neo4j/cypher-builder"; -import type { AST } from "./GraphQLInputAST/GraphQLInputAST"; +import type { UnwindASTNode } from "./GraphQLInputAST/GraphQLInputAST"; import { CreateAST, NestedCreateAST } from "./GraphQLInputAST/GraphQLInputAST"; import mapToDbProperty from "../../utils/map-to-db-property"; import type { Neo4jGraphQLTranslationContext } from "../../types/neo4j-graphql-translation-context"; @@ -40,7 +40,10 @@ function getRelationshipFields( if (relationField.interface || relationField.union) { throw new UnsupportedUnwindOptimization(`Not supported operation: Interface or Union`); } else { - refNodes.push(context.nodes.find((x) => x.name === relationField.typeMeta.name) as Node); + const node = context.nodes.find((x) => x.name === relationField.typeMeta.name); + if (node) { + refNodes.push(node); + } } } return [relationField, refNodes]; @@ -60,51 +63,41 @@ export function inputTreeToCypherMap( ) ); } - const properties = (Object.entries(input) as GraphQLCreateInput).reduce( + const properties = Object.entries(input).reduce( (obj: Record, [key, value]: [string, Record]) => { const [relationField, relatedNodes] = getRelationshipFields(node, key, context); if (relationField && relationField.properties) { - relationship = context.relationships.find( - (x) => x.properties === relationField.properties - ) as unknown as Relationship; + relationship = context.relationships.find((x) => x.properties === relationField.properties); } let scalarOrEnum = false; if (parentKey === "edge") { - scalarOrEnum = isScalarOrEnum(key, relationship as Relationship); + if (!relationship) { + throw new Error("Transpile error: relationship expected to be defined"); + } + scalarOrEnum = isScalarOrEnum(key, relationship); } // it assume that if parentKey is not defined then it means that the key belong to a Node else if (parentKey === "node" || parentKey === undefined) { scalarOrEnum = isScalarOrEnum(key, node); } if (typeof value === "object" && value !== null && (relationField || !scalarOrEnum)) { + const nodeInput = relationField ? (relatedNodes[0] as Node) : node; if (Array.isArray(value)) { obj[key] = new Cypher.List( value.map((GraphQLCreateInput: GraphQLCreateInput) => - inputTreeToCypherMap( - GraphQLCreateInput, - relationField ? (relatedNodes[0] as Node) : node, - context, - key, - relationship - ) + inputTreeToCypherMap(GraphQLCreateInput, nodeInput, context, key, relationship) ) ); return obj; } - obj[key] = inputTreeToCypherMap( - value as GraphQLCreateInput[] | GraphQLCreateInput, - relationField ? (relatedNodes[0] as Node) : node, - context, - key, - relationship - ) as Cypher.Map; + obj[key] = inputTreeToCypherMap(value, nodeInput, context, key, relationship); return obj; } obj[key] = new Cypher.Param(value); return obj; }, - {} as Record - ) as Record; + {} + ); return new Cypher.Map(properties); } @@ -127,18 +120,19 @@ export function getTreeDescriptor( parentKey?: string, relationship?: Relationship ): TreeDescriptor { - return Object.entries(input).reduce( + return Object.entries(input).reduce( (previous, [key, value]) => { const [relationField, relatedNodes] = getRelationshipFields(node, key, context); if (relationField && relationField.properties) { - relationship = context.relationships.find( - (x) => x.properties === relationField.properties - ) as unknown as Relationship; + relationship = context.relationships.find((x) => x.properties === relationField.properties); } let scalarOrEnum = false; if (parentKey === "edge") { - scalarOrEnum = isScalarOrEnum(key, relationship as Relationship); + if (!relationship) { + throw new Error("Transpile error: relationship expected to be defined"); + } + scalarOrEnum = isScalarOrEnum(key, relationship); } // it assume that if parentKey is not defined then it means that the key belong to a Node else if (parentKey === "node" || parentKey === undefined) { @@ -150,25 +144,17 @@ export function getTreeDescriptor( if (Array.isArray(value)) { previous.children[key] = mergeTreeDescriptors( - value.map((el) => - getTreeDescriptor(el as GraphQLCreateInput, innerNode, context, key, relationship) - ) + value.map((el) => getTreeDescriptor(el, innerNode, context, key, relationship)) ); return previous; } - previous.children[key] = getTreeDescriptor( - value as GraphQLCreateInput, - innerNode, - context, - key, - relationship - ); + previous.children[key] = getTreeDescriptor(value, innerNode, context, key, relationship); return previous; } previous.properties.add(key); return previous; }, - { properties: new Set(), children: {} } as TreeDescriptor + { properties: new Set(), children: {} } ); } @@ -178,30 +164,41 @@ export function mergeTreeDescriptors(input: TreeDescriptor[]): TreeDescriptor { previous.properties = new Set([...previous.properties, ...node.properties]); const entries = [...new Set([...Object.keys(previous.children), ...Object.keys(node.children)])].map( (childrenKey) => { - const previousChildren: TreeDescriptor = - previous.children[childrenKey] ?? ({ properties: new Set(), children: {} } as TreeDescriptor); - const nodeChildren: TreeDescriptor = - node.children[childrenKey] ?? ({ properties: new Set(), children: {} } as TreeDescriptor); + const previousChildren: TreeDescriptor = previous.children[childrenKey] ?? { + properties: new Set(), + children: {}, + }; + const nodeChildren: TreeDescriptor = node.children[childrenKey] ?? { + properties: new Set(), + children: {}, + }; return [childrenKey, mergeTreeDescriptors([previousChildren, nodeChildren])]; } ); previous.children = Object.fromEntries(entries); return previous; }, - { properties: new Set(), children: {} } as TreeDescriptor + { properties: new Set(), children: {} } ); } -function parser(input: TreeDescriptor, node: Node, context: Neo4jGraphQLTranslationContext, parentASTNode: AST): AST { +function parser( + input: TreeDescriptor, + node: Node, + context: Neo4jGraphQLTranslationContext, + parentASTNode: UnwindASTNode, + counter: number +): UnwindASTNode { Object.entries(input.children).forEach(([key, value]) => { const [relationField, relatedNodes] = getRelationshipFields(node, key, context); if (relationField) { let edge; if (relationField.properties) { - edge = context.relationships.find( - (x) => x.properties === relationField.properties - ) as unknown as Relationship; + edge = context.relationships.find((x) => x.properties === relationField.properties); + if (!edge) { + throw new Error("Transpile error: relationship expected to be defined"); + } } if (relationField.interface || relationField.union) { throw new UnsupportedUnwindOptimization(`Not supported operation: Interface or Union`); @@ -217,6 +214,7 @@ function parser(input: TreeDescriptor, node: Node, context: Neo4jGraphQLTranslat node, key, [relationField, relatedNodes], + counter++, edge ) ); @@ -266,12 +264,12 @@ function raiseOnNotSupportedProperty(graphElement: GraphElement) { }); } -export function parseCreate(input: TreeDescriptor, node: Node, context: Neo4jGraphQLTranslationContext) { +export function parseCreate(input: TreeDescriptor, node: Node, context: Neo4jGraphQLTranslationContext, counter = 0) { const nodeProperties = input.properties; raiseOnNotSupportedProperty(node); raiseAttributeAmbiguity(input.properties, node); - const createAST = new CreateAST([...nodeProperties], node); - parser(input, node, context, createAST); + const createAST = new CreateAST(counter++, [...nodeProperties], node); + parser(input, node, context, createAST, counter); return createAST; } @@ -282,9 +280,16 @@ function parseNestedCreate( parentNode: Node, relationshipPropertyPath: string, relationship: [RelationField | undefined, Node[]], + counter: number, edge?: Relationship ) { - const nodeProperties = (input.children.node as TreeDescriptor).properties; + if (!relationship[0]) { + throw new Error("what?"); + } + if (!input.children.node) { + throw new Error("Transpile error: node expected to be defined"); + } + const nodeProperties = input.children.node.properties; const edgeProperties = input.children.edge ? input.children.edge.properties : []; raiseOnNotSupportedProperty(node); raiseAttributeAmbiguity(nodeProperties, node); @@ -294,6 +299,7 @@ function parseNestedCreate( } const nestedCreateAST = new NestedCreateAST( + counter++, node, parentNode, [...nodeProperties], @@ -303,7 +309,7 @@ function parseNestedCreate( edge ); if (input.children.node) { - parser(input.children.node, node, context, nestedCreateAST); + parser(input.children.node, node, context, nestedCreateAST, counter); } return nestedCreateAST; } diff --git a/packages/graphql/src/translate/batch-create/types.ts b/packages/graphql/src/translate/batch-create/types.ts index 669f727c2aa..014a0782582 100644 --- a/packages/graphql/src/translate/batch-create/types.ts +++ b/packages/graphql/src/translate/batch-create/types.ts @@ -22,7 +22,7 @@ export type GraphQLCreateInput = Record; export interface TreeDescriptor { properties: Set; children: Record; - path: string; + path?: string; } export class UnsupportedUnwindOptimization extends Error { diff --git a/packages/graphql/src/translate/batch-create/unwind-create-visitors/UnwindCreateVisitor.ts b/packages/graphql/src/translate/batch-create/unwind-create-visitors/UnwindCreateVisitor.ts index 4e59ae6177f..04f095d691e 100644 --- a/packages/graphql/src/translate/batch-create/unwind-create-visitors/UnwindCreateVisitor.ts +++ b/packages/graphql/src/translate/batch-create/unwind-create-visitors/UnwindCreateVisitor.ts @@ -17,16 +17,9 @@ * limitations under the License. */ -import type { PredicateReturn, RelationField } from "../../../types"; +import type { PredicateReturn } from "../../../types"; import type { CallbackBucket } from "../../../classes/CallbackBucket"; -import type { - Visitor, - ICreateAST, - INestedCreateAST, - CreateAST, - NestedCreateAST, - IAST, -} from "../GraphQLInputAST/GraphQLInputAST"; +import type { Visitor, CreateAST, NestedCreateAST, UnwindASTNode } from "../GraphQLInputAST/GraphQLInputAST"; import type { Node, Relationship } from "../../../classes"; import createRelationshipValidationString from "../../create-relationship-validation-string"; import { filterTruthy } from "../../../utils/utils"; @@ -44,12 +37,11 @@ import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphq type UnwindCreateScopeDefinition = { unwindVar: Cypher.Variable; parentVar: Cypher.Variable; - clause?: Cypher.Clause; }; type GraphQLInputASTNodeRef = string; type UnwindCreateEnvironment = Record; -export class UnwindCreateVisitor implements Visitor { +export class UnwindCreateVisitor implements Visitor { unwindVar: Cypher.Variable; callbackBucket: CallbackBucket; context: Neo4jGraphQLTranslationContext; @@ -64,25 +56,23 @@ export class UnwindCreateVisitor implements Visitor { this.environment = {}; } - visitChildren( - currentASTNode: IAST, + public visitChildren( + currentASTNode: UnwindASTNode, unwindVar: Cypher.Variable, parentVar: Cypher.Variable - ): (Cypher.Clause | undefined)[] { + ): Cypher.Clause[] { if (currentASTNode.children) { + const scope = { unwindVar, parentVar }; const childrenRefs = currentASTNode.children.map((children) => { - this.environment[children.id] = { unwindVar, parentVar }; - children.accept(this); - return children.id; + this.environment[children.id] = scope; + return children.accept(this); }); - return childrenRefs.map( - (childrenRef) => (this.environment[childrenRef] as UnwindCreateScopeDefinition).clause - ); + return childrenRefs; } return []; } - visitCreate(create: ICreateAST): void { + public visitCreate(create: CreateAST): Cypher.Clause { const labels = create.node.getLabels(this.context); const currentNode = new Cypher.Node({ labels, @@ -97,13 +87,13 @@ export class UnwindCreateVisitor implements Visitor { const createClause = new Cypher.Create(currentNode).set(...setProperties, ...autogeneratedProperties); - const relationshipValidationClause = new Cypher.RawCypher((env: Cypher.Environment) => { + const relationshipValidationClause = new Cypher.Raw((env: Cypher.Environment) => { const validationStr = createRelationshipValidationString({ node: create.node, context: this.context, varName: env.getReferenceId(currentNode), }); - const cypher = [] as string[]; + const cypher: string[] = []; if (validationStr) { cypher.push(`WITH ${env.getReferenceId(currentNode)}`); @@ -150,11 +140,14 @@ export class UnwindCreateVisitor implements Visitor { ); this.rootNode = currentNode; this.clause = new Cypher.Call(clause).innerWith(this.unwindVar); + return this.clause; } - visitNestedCreate(nestedCreate: INestedCreateAST): void { - const parentVar = (this.environment[nestedCreate.id] as UnwindCreateScopeDefinition).parentVar; - const unwindVar = (this.environment[nestedCreate.id] as UnwindCreateScopeDefinition).unwindVar; + public visitNestedCreate(nestedCreate: NestedCreateAST): Cypher.Clause { + const scope = this.getScope(nestedCreate.id); + + const parentVar = scope.parentVar; + const unwindVar = scope.unwindVar; const { node, relationship, relationshipPropertyPath } = nestedCreate; const blockWith = new Cypher.With(parentVar, unwindVar); const createUnwindVar = new Cypher.Variable(); @@ -178,7 +171,8 @@ export class UnwindCreateVisitor implements Visitor { const nestedClauses = this.visitChildren(nestedCreate, nodeVar, currentNode); const createClause = new Cypher.Create(currentNode); - const relationField = relationship[0] as RelationField; + const relationField = relationship[0]; + if (!relationField) throw new Error("Transpile error: No relationship found"); const relationshipVar = new Cypher.Relationship({ type: relationField.type }); @@ -200,29 +194,36 @@ export class UnwindCreateVisitor implements Visitor { createClause.set(...setPropertiesNode, ...autogeneratedProperties); if (nestedCreate.edgeProperties && nestedCreate.edgeProperties.length && nestedCreate.edge) { - const setPropertiesEdge = nestedCreate.edgeProperties.map((property) => { - return fieldToSetParam( - nestedCreate.edge as Relationship, - relationshipVar, - property, - edgeVar.property(property) - ); - }); + const setPropertiesEdge = nestedCreate.edgeProperties + .map((property) => { + if (nestedCreate.edge) { + return fieldToSetParam( + nestedCreate.edge, + relationshipVar, + property, + edgeVar.property(property) + ); + } + }) + .filter((v): v is Cypher.SetParam => !!v); const autogeneratedEdgeProperties = getAutoGeneratedFields(nestedCreate.edge, relationshipVar); mergeClause.set(...setPropertiesEdge, ...autogeneratedEdgeProperties); } - const subQueryStatements = [blockWith, createUnwindClause, withCreate, createClause, mergeClause] as ( - | undefined - | Cypher.Clause - )[]; - const relationshipValidationClause = new Cypher.RawCypher((env: Cypher.Environment) => { + const subQueryStatements: Cypher.Clause[] = [ + blockWith, + createUnwindClause, + withCreate, + createClause, + mergeClause, + ]; + const relationshipValidationClause = new Cypher.Raw((env: Cypher.Environment) => { const validationStr = createRelationshipValidationString({ node, context: this.context, varName: env.getReferenceId(currentNode), }); - const cypher = [] as string[]; + const cypher: string[] = []; if (validationStr) { cypher.push(`WITH ${env.getReferenceId(currentNode)}`); cypher.push(validationStr); @@ -257,18 +258,20 @@ export class UnwindCreateVisitor implements Visitor { } subQueryStatements.push(...nestedClauses); - subQueryStatements.push(authNodeClause); - subQueryStatements.push(authorizationFieldsClause); + if (authNodeClause) { + subQueryStatements.push(authNodeClause); + } + if (authorizationFieldsClause) { + subQueryStatements.push(authorizationFieldsClause); + } + subQueryStatements.push(relationshipValidationClause); - subQueryStatements.push(new Cypher.Return([Cypher.collect(new Cypher.Literal(null)), new Cypher.Variable()])); + subQueryStatements.push(new Cypher.Return([Cypher.collect(Cypher.Null), new Cypher.Variable()])); const subQuery = Cypher.concat(...subQueryStatements); const callClause = new Cypher.Call(subQuery); const outsideWith = new Cypher.With(parentVar, unwindVar); - (this.environment[nestedCreate.id] as UnwindCreateScopeDefinition).clause = Cypher.concat( - outsideWith, - callClause - ); + return Cypher.concat(outsideWith, callClause); } private getAuthNodeClause( @@ -358,10 +361,18 @@ export class UnwindCreateVisitor implements Visitor { }; } + public getScope(identifier: number): UnwindCreateScopeDefinition { + const scope = this.environment[identifier]; + if (!scope) { + throw new Error("Transpile error: No scope found"); + } + return scope; + } + /* * Returns the Cypher Reference of the root Nodes and the Cypher Clause generated */ - build(): [Cypher.Node?, Cypher.Clause?] { + public build(): [Cypher.Node?, Cypher.Clause?] { return [this.rootNode, this.clause]; } } @@ -376,19 +387,17 @@ function getAutoGeneratedFields( ); timestampedFields.forEach((field) => { // DateTime -> datetime(); Time -> time() - const relatedCypherExpression = Cypher[field.typeMeta.name.toLowerCase()]() as Cypher.Expr; - setParams.push([ - cypherNodeRef.property(field.dbPropertyName as string), - relatedCypherExpression, - ] as Cypher.SetParam); + const relatedCypherExpression = Cypher[field.typeMeta.name.toLowerCase()](); + if (field.dbPropertyName) { + setParams.push([cypherNodeRef.property(field.dbPropertyName), relatedCypherExpression]); + } }); const autogeneratedIdFields = graphQLElement.primitiveFields.filter((x) => x.autogenerate); autogeneratedIdFields.forEach((field) => { - setParams.push([ - cypherNodeRef.property(field.dbPropertyName as string), - Cypher.randomUUID(), - ] as Cypher.SetParam); + if (field.dbPropertyName) { + setParams.push([cypherNodeRef.property(field.dbPropertyName), Cypher.randomUUID()]); + } }); return setParams; } diff --git a/packages/graphql/src/translate/create-connect-and-params.ts b/packages/graphql/src/translate/create-connect-and-params.ts index 822e3c71824..a7f8ef9b390 100644 --- a/packages/graphql/src/translate/create-connect-and-params.ts +++ b/packages/graphql/src/translate/create-connect-and-params.ts @@ -205,7 +205,7 @@ function createConnectAndParams({ const predicate = `${whereStrs.join(" AND ")}`; if (aggregationWhere) { const columns = [new Cypher.NamedVariable(nodeName)]; - const caseWhereClause = caseWhere(new Cypher.RawCypher(predicate), columns); + const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns); const { cypher } = caseWhereClause.build("aggregateWhereFilter"); subquery.push(cypher); } else { diff --git a/packages/graphql/src/translate/create-connect-or-create-and-params.ts b/packages/graphql/src/translate/create-connect-or-create-and-params.ts index 4f73106d286..e1496af49cb 100644 --- a/packages/graphql/src/translate/create-connect-or-create-and-params.ts +++ b/packages/graphql/src/translate/create-connect-or-create-and-params.ts @@ -104,7 +104,7 @@ export function createConnectOrCreateAndParams({ const wrappedQueries = statements.map((statement) => { const returnStatement = context.subscriptionsEnabled ? new Cypher.Return([new Cypher.NamedVariable("meta"), "update_meta"]) - : new Cypher.Return([Cypher.count(new Cypher.RawCypher("*")), "_"]); + : new Cypher.Return([Cypher.count(new Cypher.Raw("*")), "_"]); const withStatement = new Cypher.With(...withVarsVariables); @@ -250,7 +250,7 @@ function mergeStatement({ const callbackFields = getCallbackFields(refNode); const callbackParams = callbackFields - .map((callbackField): [Cypher.Property, Cypher.RawCypher] | [] => { + .map((callbackField): [Cypher.Property, Cypher.Raw] | [] => { const varNameVariable = new Cypher.NamedVariable(varName); return addCallbackAndSetParamCypher( callbackField, @@ -261,7 +261,7 @@ function mergeStatement({ node ); }) - .filter((tuple) => tuple.length !== 0) as [Cypher.Property, Cypher.RawCypher][]; + .filter((tuple) => tuple.length !== 0) as [Cypher.Property, Cypher.Raw][]; const rawNodeParams = { ...unsetAutogeneratedParams, @@ -304,7 +304,7 @@ function mergeStatement({ relationField.direction === "IN" ? [refNode.name, parentRefNode.name] : [parentRefNode.name, refNode.name]; const [fromNode, toNode] = relationField.direction === "IN" ? [node, parentNode] : [parentNode, node]; - withClause = new Cypher.RawCypher((env: Cypher.Environment) => { + withClause = new Cypher.Raw((env: Cypher.Environment) => { const eventWithMetaStr = createConnectionEventMeta({ event: "create_relationship", relVariable: compileCypher(relationship, env), @@ -443,7 +443,7 @@ function getAutogeneratedParams(node: Node | Relationship): Record f.autogenerate) .reduce((acc, field) => { if (field.dbPropertyName) { - acc[field.dbPropertyName] = new Cypher.RawCypher("randomUUID()"); + acc[field.dbPropertyName] = new Cypher.Raw("randomUUID()"); } return acc; }, {}); @@ -452,7 +452,7 @@ function getAutogeneratedParams(node: Node | Relationship): Record ["DateTime", "Time"].includes(field.typeMeta.name) && field.timestamps?.includes("CREATE")) .reduce((acc, field) => { if (field.dbPropertyName) { - acc[field.dbPropertyName] = new Cypher.RawCypher(`${field.typeMeta.name.toLowerCase()}()`); + acc[field.dbPropertyName] = new Cypher.Raw(`${field.typeMeta.name.toLowerCase()}()`); } return acc; }, {}); diff --git a/packages/graphql/src/translate/create-create-and-params.ts b/packages/graphql/src/translate/create-create-and-params.ts index c363e2a108d..76adb7c889c 100644 --- a/packages/graphql/src/translate/create-create-and-params.ts +++ b/packages/graphql/src/translate/create-create-and-params.ts @@ -63,12 +63,7 @@ function createCreateAndParams({ withVars, includeRelationshipValidation, topLevelNodeVariable, - authorizationPrefix = { - inputIndex: 0, - reducerIndex: 0, - createIndex: 0, - refNodeIndex: 0, - }, + authorizationPrefix = [0], }: { input: any; varName: string; @@ -79,12 +74,7 @@ function createCreateAndParams({ includeRelationshipValidation?: boolean; topLevelNodeVariable?: string; //used to build authorization variable in auth subqueries - authorizationPrefix?: { - inputIndex: number; // index of the input - reducerIndex: number; // index of the reducer in the context of the input - createIndex: number; // index of the create in the context of a run of the reducer - refNodeIndex: number; // when a relationship is to an abstract type, this is the index of the refNode in the context of a run of the reducer - }; + authorizationPrefix?: number[]; }): CreateAndParams { const conflictingProperties = findConflictingProperties({ node, input }); if (conflictingProperties.length > 0) { @@ -169,12 +159,7 @@ function createCreateAndParams({ withVars: [...withVars, nodeName], includeRelationshipValidation: false, topLevelNodeVariable, - authorizationPrefix: { - inputIndex: authorizationPrefix.inputIndex + 1, - reducerIndex, - createIndex, - refNodeIndex, - }, + authorizationPrefix: [...authorizationPrefix, reducerIndex, createIndex, refNodeIndex], }); res.creates.push(nestedCreate); res.params = { ...res.params, ...params }; @@ -408,13 +393,8 @@ function createCreateAndParams({ return { create: creates.join("\n"), params, authorizationPredicates, authorizationSubqueries }; } -function makeAuthorizationParamsPrefix(authorizationPrefix: { - inputIndex: number; - reducerIndex: number; - createIndex: number; - refNodeIndex: number; -}): string { - return `${authorizationPrefix.inputIndex}_${authorizationPrefix.reducerIndex}_${authorizationPrefix.refNodeIndex}_${authorizationPrefix.createIndex}_`; +function makeAuthorizationParamsPrefix(authorizationPrefix: number[]): string { + return `${authorizationPrefix.join("_")}_`; } export default createCreateAndParams; diff --git a/packages/graphql/src/translate/create-delete-and-params.ts b/packages/graphql/src/translate/create-delete-and-params.ts index 3374dab1b26..0103ed0d05c 100644 --- a/packages/graphql/src/translate/create-delete-and-params.ts +++ b/packages/graphql/src/translate/create-delete-and-params.ts @@ -180,7 +180,7 @@ function createDeleteAndParams({ new Cypher.NamedVariable(relationshipVariable), new Cypher.NamedVariable(variableName), ]; - const caseWhereClause = caseWhere(new Cypher.RawCypher(predicate), columns); + const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns); const { cypher } = caseWhereClause.build("aggregateWhereFilter"); innerStrs.push(cypher); } else { diff --git a/packages/graphql/src/translate/create-disconnect-and-params.ts b/packages/graphql/src/translate/create-disconnect-and-params.ts index f147b7dc70f..5d2032bacf2 100644 --- a/packages/graphql/src/translate/create-disconnect-and-params.ts +++ b/packages/graphql/src/translate/create-disconnect-and-params.ts @@ -140,7 +140,7 @@ function createDisconnectAndParams({ const predicate = `${whereStrs.join(" AND ")}`; if (aggregationWhere) { const columns = [new Cypher.NamedVariable(relVarName), new Cypher.NamedVariable(variableName)]; - const caseWhereClause = caseWhere(new Cypher.RawCypher(predicate), columns); + const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns); const { cypher } = caseWhereClause.build("aggregateWhereFilter"); subquery.push(cypher); } else { diff --git a/packages/graphql/src/translate/create-projection-and-params.test.ts b/packages/graphql/src/translate/create-projection-and-params.test.ts index df0e46db925..f114d3ce6cc 100644 --- a/packages/graphql/src/translate/create-projection-and-params.test.ts +++ b/packages/graphql/src/translate/create-projection-and-params.test.ts @@ -118,7 +118,7 @@ describe("createProjectionAndParams", () => { varName: new Cypher.NamedNode("this"), cypherFieldAliasMap: {}, }); - new Cypher.RawCypher((env) => { + new Cypher.Raw((env) => { expect(compileCypher(result.projection, env)).toBe(`{ .title }`); return ""; }).build(); @@ -213,7 +213,7 @@ describe("createProjectionAndParams", () => { varName: new Cypher.NamedNode("this"), cypherFieldAliasMap: {}, }); - new Cypher.RawCypher((env) => { + new Cypher.Raw((env) => { expect(compileCypher(result.projection, env)).toBe(`{ .title }`); return ""; }).build(); diff --git a/packages/graphql/src/translate/create-projection-and-params.ts b/packages/graphql/src/translate/create-projection-and-params.ts index f643c87562f..45f037bd591 100644 --- a/packages/graphql/src/translate/create-projection-and-params.ts +++ b/packages/graphql/src/translate/create-projection-and-params.ts @@ -196,7 +196,7 @@ export default function createProjectionAndParams({ const direction = getCypherRelationshipDirection(relationField, field.args); - const nestedProjection = new Cypher.RawCypher((env) => { + const nestedProjection = new Cypher.Raw((env) => { // The nested projection will be surrounded by brackets, so we want to remove // any linebreaks, and then the first opening and the last closing bracket of the line, // as well as any surrounding whitespace. @@ -241,9 +241,7 @@ export default function createProjectionAndParams({ const unionAndSort = Cypher.concat(new Cypher.Call(unionClause), collectAndLimitStatements); res.subqueries.push(new Cypher.Call(unionAndSort).innerWith(parentNode)); - res.projection.push( - new Cypher.RawCypher((env) => `${alias}: ${compileCypher(subqueryReturnAlias, env)}`) - ); + res.projection.push(new Cypher.Raw((env) => `${alias}: ${compileCypher(subqueryReturnAlias, env)}`)); return res; } @@ -280,7 +278,7 @@ export default function createProjectionAndParams({ nestedPredicates: recurse.predicates, }); res.subqueries.push(new Cypher.Call(subquery).innerWith(varName)); - res.projection.push(new Cypher.RawCypher((env) => `${alias}: ${compileCypher(subqueryReturnAlias, env)}`)); + res.projection.push(new Cypher.Raw((env) => `${alias}: ${compileCypher(subqueryReturnAlias, env)}`)); return res; } @@ -296,9 +294,7 @@ export default function createProjectionAndParams({ res.subqueries.push(aggregationFieldProjection.projectionSubqueryCypher); } res.projection.push( - new Cypher.RawCypher( - (env) => `${alias}: ${compileCypher(aggregationFieldProjection.projectionCypher, env)}` - ) + new Cypher.Raw((env) => `${alias}: ${compileCypher(aggregationFieldProjection.projectionCypher, env)}`) ); return res; } @@ -317,21 +313,21 @@ export default function createProjectionAndParams({ ).innerWith(varName); res.subqueries.push(connectionClause); - res.projection.push(new Cypher.RawCypher((env) => `${field.alias}: ${compileCypher(returnVariable, env)}`)); + res.projection.push(new Cypher.Raw((env) => `${field.alias}: ${compileCypher(returnVariable, env)}`)); return res; } if (pointField) { const pointExpr = createPointExpression({ resolveTree: field, field: pointField, variable: varName }); - res.projection.push(new Cypher.RawCypher((env) => `${field.alias}: ${compileCypher(pointExpr, env)}`)); + res.projection.push(new Cypher.Raw((env) => `${field.alias}: ${compileCypher(pointExpr, env)}`)); } else if (temporalField?.typeMeta.name === "DateTime") { const datetimeExpr = createDatetimeExpression({ resolveTree: field, field: temporalField, variable: varName, }); - res.projection.push(new Cypher.RawCypher((env) => `${field.alias}: ${compileCypher(datetimeExpr, env)}`)); + res.projection.push(new Cypher.Raw((env) => `${field.alias}: ${compileCypher(datetimeExpr, env)}`)); } else { // In the case of using the @alias directive (map a GraphQL field to a db prop) // the output will be RETURN varName {GraphQLfield: varName.dbAlias} @@ -342,10 +338,10 @@ export default function createProjectionAndParams({ if (alias !== field.name || dbFieldName !== field.name || literalElements) { res.projection.push( - new Cypher.RawCypher((env) => `${alias}: ${compileCypher(varName.property(dbFieldName), env)}`) + new Cypher.Raw((env) => `${alias}: ${compileCypher(varName.property(dbFieldName), env)}`) ); } else { - res.projection.push(new Cypher.RawCypher(`.${dbFieldName}`)); + res.projection.push(new Cypher.Raw(`.${dbFieldName}`)); } } @@ -411,8 +407,8 @@ export default function createProjectionAndParams({ { projection: resolveType ? [ - new Cypher.RawCypher(`__resolveType: "${node.name}"`), - new Cypher.RawCypher((env) => `__id: id(${compileCypher(varName, env)})`), + new Cypher.Raw(`__resolveType: "${node.name}"`), + new Cypher.Raw((env) => `__id: id(${compileCypher(varName, env)})`), ] : [], params: {}, @@ -421,7 +417,7 @@ export default function createProjectionAndParams({ predicates: [], } ); - const projectionCypher = new Cypher.RawCypher((env) => { + const projectionCypher = new Cypher.Raw((env) => { return `{ ${projection.map((proj) => compileCypher(proj, env)).join(", ")} }`; }); return { diff --git a/packages/graphql/src/translate/create-update-and-params.ts b/packages/graphql/src/translate/create-update-and-params.ts index 0f136428d65..72eb9b1b215 100644 --- a/packages/graphql/src/translate/create-update-and-params.ts +++ b/packages/graphql/src/translate/create-update-and-params.ts @@ -263,7 +263,7 @@ export default function createUpdateAndParams({ new Cypher.NamedVariable(relationshipVariable), new Cypher.NamedVariable(variableName), ]; - const caseWhereClause = caseWhere(new Cypher.RawCypher(predicate), columns); + const caseWhereClause = caseWhere(new Cypher.Raw(predicate), columns); const { cypher } = caseWhereClause.build("aggregateWhereFilter"); innerUpdate.push(cypher); } else { diff --git a/packages/graphql/src/translate/field-aggregations/aggregation-sub-queries.ts b/packages/graphql/src/translate/field-aggregations/aggregation-sub-queries.ts index 35ebd487005..1c7d6965096 100644 --- a/packages/graphql/src/translate/field-aggregations/aggregation-sub-queries.ts +++ b/packages/graphql/src/translate/field-aggregations/aggregation-sub-queries.ts @@ -44,7 +44,7 @@ export function stringAggregationQuery( ): Cypher.Clause { const fieldPath = targetAlias.property(fieldName); - return new Cypher.RawCypher((env) => { + return new Cypher.Raw((env) => { const targetAliasCypher = compileCypher(targetAlias, env); const fieldPathCypher = compileCypher(fieldPath, env); @@ -61,9 +61,9 @@ export function numberAggregationQuery( fieldName: string, fieldRef: Cypher.Variable, targetAlias: Cypher.Node | Cypher.Relationship -): Cypher.RawCypher { +): Cypher.Raw { const fieldPath = targetAlias.property(fieldName); - return new Cypher.RawCypher((env) => { + return new Cypher.Raw((env) => { const fieldPathCypher = compileCypher(fieldPath, env); return dedent`${compileCypher(matchWherePattern, env)} @@ -79,9 +79,9 @@ export function defaultAggregationQuery( fieldName: string, fieldRef: Cypher.Variable, targetAlias: Cypher.Node | Cypher.Relationship -): Cypher.RawCypher { +): Cypher.Raw { const fieldPath = targetAlias.property(fieldName); - return new Cypher.RawCypher((env) => { + return new Cypher.Raw((env) => { const fieldPathCypher = compileCypher(fieldPath, env); return dedent`${compileCypher(matchWherePattern, env)} @@ -94,14 +94,14 @@ export function dateTimeAggregationQuery( fieldName: string, fieldRef: Cypher.Variable, targetAlias: Cypher.Node | Cypher.Relationship -): Cypher.RawCypher { +): Cypher.Raw { const fieldPath = targetAlias.property(fieldName); - return new Cypher.RawCypher((env) => { + return new Cypher.Raw((env) => { const fieldPathCypher = compileCypher(fieldPath, env); return dedent`${compileCypher(matchWherePattern, env)} RETURN ${stringifyObject({ - min: new Cypher.RawCypher(wrapApocConvertDate(`min(${fieldPathCypher})`)), - max: new Cypher.RawCypher(wrapApocConvertDate(`max(${fieldPathCypher})`)), + min: new Cypher.Raw(wrapApocConvertDate(`min(${fieldPathCypher})`)), + max: new Cypher.Raw(wrapApocConvertDate(`max(${fieldPathCypher})`)), }).getCypher(env)} AS ${compileCypher(fieldRef, env)}`; }); } diff --git a/packages/graphql/src/translate/field-aggregations/create-field-aggregation.ts b/packages/graphql/src/translate/field-aggregations/create-field-aggregation.ts index 87add712aeb..392f3a52167 100644 --- a/packages/graphql/src/translate/field-aggregations/create-field-aggregation.ts +++ b/packages/graphql/src/translate/field-aggregations/create-field-aggregation.ts @@ -165,7 +165,7 @@ export function createFieldAggregation({ return { projectionCypher: projectionMap, - projectionSubqueryCypher: projectionSubqueries || new Cypher.RawCypher(""), + projectionSubqueryCypher: projectionSubqueries || new Cypher.Raw(""), }; } @@ -204,7 +204,7 @@ function getAggregationProjectionAndSubqueries({ ); }); if (!innerProjectionSubqueries) { - innerProjectionSubqueries = new Cypher.RawCypher(""); + innerProjectionSubqueries = new Cypher.Raw(""); } return { innerProjectionMap, innerProjectionSubqueries }; } diff --git a/packages/graphql/src/translate/index.ts b/packages/graphql/src/translate/index.ts index f225a95db1b..299bbb81edd 100644 --- a/packages/graphql/src/translate/index.ts +++ b/packages/graphql/src/translate/index.ts @@ -17,9 +17,9 @@ * limitations under the License. */ +export { translateAggregate } from "./translate-aggregate"; export { default as translateCreate } from "./translate-create"; -export { translateRead } from "./translate-read"; -export { default as translateUpdate } from "./translate-update"; export { translateDelete } from "./translate-delete"; -export { default as translateAggregate } from "./translate-aggregate"; +export { translateRead } from "./translate-read"; export { translateTopLevelCypher } from "./translate-top-level-cypher"; +export { default as translateUpdate } from "./translate-update"; diff --git a/packages/graphql/src/translate/projection/elements/create-datetime-element.test.ts b/packages/graphql/src/translate/projection/elements/create-datetime-element.test.ts index 580d7d07ac8..6f38ab83306 100644 --- a/packages/graphql/src/translate/projection/elements/create-datetime-element.test.ts +++ b/packages/graphql/src/translate/projection/elements/create-datetime-element.test.ts @@ -44,7 +44,7 @@ describe("createDatetimeElement", () => { field, variable: new Cypher.NamedVariable("this"), }); - new Cypher.RawCypher((env) => { + new Cypher.Raw((env) => { expect(compileCypher(element, env)).toBe( 'datetime: apoc.date.convertFormat(toString(this.datetime), "iso_zoned_date_time", "iso_offset_date_time")' ); @@ -73,7 +73,7 @@ describe("createDatetimeElement", () => { field, variable: new Cypher.NamedVariable("this"), }); - new Cypher.RawCypher((env) => { + new Cypher.Raw((env) => { expect(compileCypher(element, env)).toBe( 'datetimes: [ dt in this.datetimes | apoc.date.convertFormat(toString(dt), "iso_zoned_date_time", "iso_offset_date_time") ]' ); diff --git a/packages/graphql/src/translate/projection/elements/create-datetime-element.ts b/packages/graphql/src/translate/projection/elements/create-datetime-element.ts index e157d8a9fa1..2e98435d2c7 100644 --- a/packages/graphql/src/translate/projection/elements/create-datetime-element.ts +++ b/packages/graphql/src/translate/projection/elements/create-datetime-element.ts @@ -35,7 +35,7 @@ export function createDatetimeElement({ valueOverride?: string; }): Cypher.Expr { const dbFieldName = field.dbPropertyName || resolveTree.name; - return new Cypher.RawCypher((env) => + return new Cypher.Raw((env) => field.typeMeta.array ? `${resolveTree.alias}: [ dt in ${compileCypher(variable, env)}.${dbFieldName} | ${wrapApocConvertDate( "dt" diff --git a/packages/graphql/src/translate/projection/elements/create-point-element.test.ts b/packages/graphql/src/translate/projection/elements/create-point-element.test.ts index 4f10db72bb2..6650bde1e62 100644 --- a/packages/graphql/src/translate/projection/elements/create-point-element.test.ts +++ b/packages/graphql/src/translate/projection/elements/create-point-element.test.ts @@ -60,7 +60,7 @@ describe("createPointElement", () => { variable: new Cypher.NamedVariable("this"), }); - new Cypher.RawCypher((env) => { + new Cypher.Raw((env) => { expect(compileCypher(element, env)).toMatchInlineSnapshot(` "point: (CASE WHEN this.point IS NOT NULL THEN { point: this.point, crs: this.point.crs } @@ -107,7 +107,7 @@ describe("createPointElement", () => { field, variable: new Cypher.NamedVariable("this"), }); - new Cypher.RawCypher((env) => { + new Cypher.Raw((env) => { expect(compileCypher(element, env)).toMatchInlineSnapshot(` "points: (CASE WHEN this.points IS NOT NULL THEN [p_var0 IN this.points | { point: p_var0, crs: p_var0.crs }] diff --git a/packages/graphql/src/translate/projection/elements/create-point-element.ts b/packages/graphql/src/translate/projection/elements/create-point-element.ts index a01f56c2748..9f6b82fa485 100644 --- a/packages/graphql/src/translate/projection/elements/create-point-element.ts +++ b/packages/graphql/src/translate/projection/elements/create-point-element.ts @@ -34,11 +34,11 @@ export default function createPointElement({ }): Cypher.Expr { const expression = createPointExpression({ resolveTree, field, variable }); - const cypherClause = new Cypher.RawCypher((env) => { + const cypherClause = new Cypher.Raw((env) => { return compileCypher(expression, env); }); const { cypher } = cypherClause.build("p_"); - return new Cypher.RawCypher(`${resolveTree.alias}: (${cypher})`); + return new Cypher.Raw(`${resolveTree.alias}: (${cypher})`); } export function createPointExpression({ diff --git a/packages/graphql/src/translate/projection/elements/create-relationship-property-value.test.ts b/packages/graphql/src/translate/projection/elements/create-relationship-property-value.test.ts index ccef42e7d37..b12c40de964 100644 --- a/packages/graphql/src/translate/projection/elements/create-relationship-property-value.test.ts +++ b/packages/graphql/src/translate/projection/elements/create-relationship-property-value.test.ts @@ -72,8 +72,6 @@ describe("createRelationshipPropertyElement", () => { otherDirectives: [], arguments: [], description: undefined, - readonly: false, - writeonly: false, } as PrimitiveField, ], pointFields: [ @@ -115,8 +113,6 @@ describe("createRelationshipPropertyElement", () => { otherDirectives: [], arguments: [], description: undefined, - readonly: false, - writeonly: false, } as PointField, ], temporalFields: [ @@ -158,8 +154,6 @@ describe("createRelationshipPropertyElement", () => { otherDirectives: [], arguments: [], description: undefined, - readonly: false, - writeonly: false, } as TemporalField, ], }); @@ -178,7 +172,7 @@ describe("createRelationshipPropertyElement", () => { relationship, relationshipVariable: new Cypher.Relationship(), }); - new Cypher.RawCypher((env) => { + new Cypher.Raw((env) => { expect(compileCypher(element, env)).toMatchInlineSnapshot(`"this0.int"`); return ""; }).build(); @@ -197,7 +191,7 @@ describe("createRelationshipPropertyElement", () => { relationship, relationshipVariable: new Cypher.Relationship(), }); - new Cypher.RawCypher((env) => { + new Cypher.Raw((env) => { expect(compileCypher(element, env)).toMatchInlineSnapshot( `"apoc.date.convertFormat(toString(this0.datetime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\")"` ); @@ -233,7 +227,7 @@ describe("createRelationshipPropertyElement", () => { relationship, relationshipVariable: new Cypher.Relationship(), }); - new Cypher.RawCypher((env) => { + new Cypher.Raw((env) => { expect(compileCypher(element, env)).toMatchInlineSnapshot(` "CASE WHEN this0.point IS NOT NULL THEN { point: this0.point, crs: this0.point.crs } diff --git a/packages/graphql/src/translate/projection/subquery/create-projection-subquery.ts b/packages/graphql/src/translate/projection/subquery/create-projection-subquery.ts index 74e60538bd3..484d97b81db 100644 --- a/packages/graphql/src/translate/projection/subquery/create-projection-subquery.ts +++ b/packages/graphql/src/translate/projection/subquery/create-projection-subquery.ts @@ -73,7 +73,7 @@ export function createProjectionSubquery({ const subqueryMatch = new Cypher.Match(pattern); const predicates = nestedPredicates; - const projection = new Cypher.RawCypher((env) => { + const projection = new Cypher.Raw((env) => { // TODO: use MapProjection return `${compileCypher(targetNode, env)} ${compileCypher(nestedProjection, env)}`; }); diff --git a/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts b/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts index 4023a9efbf2..21d042eb0f7 100644 --- a/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts +++ b/packages/graphql/src/translate/projection/subquery/translate-cypher-directive-projection.ts @@ -86,7 +86,7 @@ export function translateCypherDirectiveProjection({ cypherFieldAliasMap, }); - projectionExpr = new Cypher.RawCypher((env) => { + projectionExpr = new Cypher.Raw((env) => { return `${compileCypher(resultVariable, env)} ${compileCypher(str, env)}`; }); res.params = { ...res.params, ...p }; @@ -137,7 +137,7 @@ export function translateCypherDirectiveProjection({ const withAndSubqueries = Cypher.concat(beforeCallWith, ...nestedSubqueries); subqueries.push(withAndSubqueries); } - const projection = new Cypher.RawCypher( + const projection = new Cypher.Raw( (env) => `{ __resolveType: "${refNode.name}", ${compileCypher(str, env).replace("{", "")}` ); unionProjections.push({ @@ -149,7 +149,7 @@ export function translateCypherDirectiveProjection({ res.params = { ...res.params, ...p }; } else { - const projection = new Cypher.RawCypher(() => `{ __resolveType: "${refNode.name}" }`); + const projection = new Cypher.Raw(() => `{ __resolveType: "${refNode.name}" }`); unionProjections.push({ projection, predicate: labelsSubPredicate, @@ -164,9 +164,7 @@ export function translateCypherDirectiveProjection({ projectionExpr .when(predicate) .then( - new Cypher.RawCypher( - (env) => `${compileCypher(resultVariable, env)} ${compileCypher(projection, env)}` - ) + new Cypher.Raw((env) => `${compileCypher(resultVariable, env)} ${compileCypher(projection, env)}`) ); } } @@ -212,7 +210,7 @@ export function translateCypherDirectiveProjection({ } const aliasVar = new Cypher.NamedVariable(alias); res.projection.push( - new Cypher.RawCypher((env) => `${compileCypher(aliasVar, env)}: ${compileCypher(resultVariable, env)}`) + new Cypher.Raw((env) => `${compileCypher(aliasVar, env)}: ${compileCypher(resultVariable, env)}`) ); return res; } @@ -229,7 +227,7 @@ function createCypherDirectiveSubquery({ extraArgs: Record; }): Cypher.Clause { const innerWithAlias = new Cypher.With([nodeRef, new Cypher.NamedNode("this")]); - const rawCypher = new Cypher.RawCypher((env) => { + const rawCypher = new Cypher.Raw((env) => { let statement = cypherField.statement; for (const [key, value] of Object.entries(extraArgs)) { const param = new Cypher.Param(value); diff --git a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts index 24fd7d6a6cb..85f79cc3745 100644 --- a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts +++ b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts @@ -43,8 +43,14 @@ export class QueryAST { public buildNew(neo4jGraphQLContext: Neo4jGraphQLTranslationContext, varName?: string): Cypher.Clause { const context = this.buildQueryASTContext(neo4jGraphQLContext, varName); - const { clauses, projectionExpr } = this.transpile(context); - const returnClause = new Cypher.Return(projectionExpr); + const { clauses, projectionExpr, extraProjectionColumns } = this.transpile(context); + const returnClause = varName ? new Cypher.Return([projectionExpr, varName]) : new Cypher.Return(projectionExpr); + + if (extraProjectionColumns) { + for (const projectionColumn of extraProjectionColumns) { + returnClause.addColumns(projectionColumn); + } + } return Cypher.concat(...clauses, returnClause); } diff --git a/packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/CypherUnionAttributePartial.ts b/packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/CypherUnionAttributePartial.ts index 87a94949303..07b22075074 100644 --- a/packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/CypherUnionAttributePartial.ts +++ b/packages/graphql/src/translate/queryAST/ast/fields/attribute-fields/CypherUnionAttributePartial.ts @@ -55,7 +55,7 @@ export class CypherUnionAttributePartial extends QueryASTNode { // TODO: Refactor when `.hasLabel` on variables is supported in CypherBuilder const predicates = labels.map((label) => { - return new Cypher.RawCypher((env) => { + return new Cypher.Raw((env) => { const varName = env.compile(variable); const labelStr = Cypher.utils.escapeLabel(label); return `${varName}:${labelStr}`; diff --git a/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts index 879772cc44f..07b4aca95b5 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/ConnectionFilter.ts @@ -107,23 +107,25 @@ export class ConnectionFilter extends Filter { public getPredicate(queryASTContext: QueryASTContext): Cypher.Predicate | undefined { if (!hasTarget(queryASTContext)) throw new Error("No parent node found!"); - if (this.subqueryPredicate) return this.subqueryPredicate; - else { - const target = this.getTargetNode(queryASTContext); - const relationship = new Cypher.Relationship({ - type: this.relationship.type, - }); + if (this.subqueryPredicate) { + return this.subqueryPredicate; + } - const pattern = new Cypher.Pattern(queryASTContext.target) - .withoutLabels() - .related(relationship) - .withDirection(this.relationship.getCypherDirection()) - .to(target); + const target = this.getTargetNode(queryASTContext); + const relationship = new Cypher.Relationship({ + type: this.relationship.type, + }); - const nestedContext = queryASTContext.push({ target, relationship }); + const pattern = new Cypher.Pattern(queryASTContext.target) + .withoutLabels() + .related(relationship) + .withDirection(this.relationship.getCypherDirection()) + .to(target); + + const nestedContext = queryASTContext.push({ target, relationship }); - const predicate = this.createRelationshipOperation(pattern, nestedContext); - if (!predicate) return undefined; + const predicate = this.createRelationshipOperation(pattern, nestedContext); + if (predicate) { return this.wrapInNotIfNeeded(predicate); } } @@ -202,14 +204,15 @@ export class ConnectionFilter extends Filter { const nestedSubqueries = f .getSubqueries(queryASTContext) .map((sq) => new Cypher.Call(sq).innerWith(queryASTContext.target)); - + const selection = f.getSelection(queryASTContext); const predicate = f.getPredicate(queryASTContext); + const clauses = [...selection, ...nestedSubqueries]; if (predicate) { innerFiltersPredicates.push(predicate); - return nestedSubqueries; + return clauses; } - return nestedSubqueries; + return clauses; }); if (subqueries.length === 0) return []; // Hack logic to change predicates logic diff --git a/packages/graphql/src/translate/queryAST/ast/filters/RelationshipFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/RelationshipFilter.ts index e3f4a86d865..9826688c89f 100644 --- a/packages/graphql/src/translate/queryAST/ast/filters/RelationshipFilter.ts +++ b/packages/graphql/src/translate/queryAST/ast/filters/RelationshipFilter.ts @@ -18,22 +18,25 @@ */ import Cypher from "@neo4j/cypher-builder"; -import type { RelationshipWhereOperator } from "../../../where/types"; -import { Filter } from "./Filter"; -import type { QueryASTContext } from "../QueryASTContext"; -import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; -import type { QueryASTNode } from "../QueryASTNode"; import { Memoize } from "typescript-memoize"; +import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { InterfaceEntityAdapter } from "../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; +import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { filterTruthy } from "../../../../utils/utils"; +import type { RelationshipWhereOperator } from "../../../where/types"; import { hasTarget } from "../../utils/context-has-target"; -import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls"; import { createNodeFromEntity } from "../../utils/create-node-from-entity"; +import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls"; +import type { QueryASTContext } from "../QueryASTContext"; +import type { QueryASTNode } from "../QueryASTNode"; +import { Filter } from "./Filter"; export class RelationshipFilter extends Filter { protected targetNodeFilters: Filter[] = []; protected relationship: RelationshipAdapter; protected operator: RelationshipWhereOperator; protected isNot: boolean; + protected target: ConcreteEntityAdapter | InterfaceEntityAdapter; // TODO: remove this, this is not good protected subqueryPredicate: Cypher.Predicate | undefined; @@ -45,15 +48,18 @@ export class RelationshipFilter extends Filter { relationship, operator, isNot, + target, }: { relationship: RelationshipAdapter; operator: RelationshipWhereOperator; isNot: boolean; + target: ConcreteEntityAdapter | InterfaceEntityAdapter; }) { super(); this.relationship = relationship; this.isNot = isNot; this.operator = operator; + this.target = target; // Note: This is just to keep naming with previous Cypher, it is safe to remove this.countVariable = new Cypher.NamedVariable(`${this.relationship.name}Count`); @@ -73,7 +79,7 @@ export class RelationshipFilter extends Filter { @Memoize() protected getNestedContext(context: QueryASTContext): QueryASTContext { - const relatedEntity = this.relationship.target; + const relatedEntity = this.target; const target = createNodeFromEntity(relatedEntity, context.neo4jGraphQLContext); const relationship = new Cypher.Relationship({ type: this.relationship.type, @@ -134,7 +140,7 @@ export class RelationshipFilter extends Filter { public getSubqueries(context: QueryASTContext): Cypher.Clause[] { // NOTE: not using getNestedContext because this should not be memoized in ALL operations - const relatedEntity = this.relationship.target; + const relatedEntity = this.target; const target = createNodeFromEntity(relatedEntity, context.neo4jGraphQLContext); const relationship = new Cypher.Relationship({ type: this.relationship.type, diff --git a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/TypenameFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/TypenameFilter.ts new file mode 100644 index 00000000000..6df5eacd55b --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/TypenameFilter.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Cypher from "@neo4j/cypher-builder"; +import type { QueryASTContext } from "../../QueryASTContext"; +import type { QueryASTNode } from "../../QueryASTNode"; +import { Filter } from "../Filter"; +import type { ConcreteEntityAdapter } from "../../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import { hasTarget } from "../../../utils/context-has-target"; + +export class TypenameFilter extends Filter { + private readonly acceptedEntities: ConcreteEntityAdapter[]; + + constructor(acceptedEntities: ConcreteEntityAdapter[]) { + super(); + this.acceptedEntities = acceptedEntities; + } + + public getChildren(): QueryASTNode[] { + return []; + } + + public print(): string { + const acceptedEntities = this.acceptedEntities.map((e) => e.name); + return `${super.print()} [${acceptedEntities.join(", ")}]`; + } + + public getPredicate(queryASTContext: QueryASTContext): Cypher.Predicate { + if (!hasTarget(queryASTContext)) throw new Error("No parent node found!"); + const labelPredicate = this.acceptedEntities.map((e) => { + return queryASTContext.target.hasLabels(e.name); + }); + return Cypher.or(...labelPredicate); + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts index f4808d8ef00..d91fd0dc7b9 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts @@ -22,7 +22,6 @@ import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/mode import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { filterTruthy } from "../../../../utils/utils"; import { hasTarget } from "../../utils/context-has-target"; -import { createNodeFromEntity } from "../../utils/create-node-from-entity"; import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls"; import type { QueryASTContext } from "../QueryASTContext"; import type { QueryASTNode } from "../QueryASTNode"; @@ -30,7 +29,8 @@ import type { Field } from "../fields/Field"; import { CypherAttributeField } from "../fields/attribute-fields/CypherAttributeField"; import type { Filter } from "../filters/Filter"; import type { AuthorizationFilters } from "../filters/authorization-filters/AuthorizationFilters"; -import type { Pagination, PaginationField } from "../pagination/Pagination"; +import type { Pagination } from "../pagination/Pagination"; +import type { EntitySelection } from "../selection/EntitySelection"; import { CypherPropertySort } from "../sort/CypherPropertySort"; import type { Sort, SortField } from "../sort/Sort"; import type { OperationTranspileResult } from "./operations"; @@ -49,19 +49,24 @@ export class ConnectionReadOperation extends Operation { protected authFilters: AuthorizationFilters[] = []; public nodeAlias: string | undefined; // This is just to maintain naming with the old way (this), remove after refactor + protected selection: EntitySelection; + constructor({ relationship, directed, target, + selection, }: { relationship: RelationshipAdapter | undefined; target: ConcreteEntityAdapter; directed: boolean; + selection: EntitySelection; }) { super(); this.relationship = relationship; this.directed = directed; this.target = target; + this.selection = selection; } public setNodeFields(fields: Field[]) { @@ -80,14 +85,6 @@ export class ConnectionReadOperation extends Operation { this.authFilters.push(...filter); } - protected getAuthFilterSubqueries(context: QueryASTContext): Cypher.Clause[] { - return this.authFilters.flatMap((f) => f.getSubqueries(context)); - } - - protected getAuthFilterPredicate(context: QueryASTContext): Cypher.Predicate[] { - return filterTruthy(this.authFilters.map((f) => f.getPredicate(context))); - } - public addSort(sortElement: { node: Sort[]; edge: Sort[] }): void { this.sortFields.push(sortElement); } @@ -111,305 +108,222 @@ export class ConnectionReadOperation extends Operation { ]); } - protected getSelectionClauses( - context: QueryASTContext, - node: Cypher.Node | Cypher.Pattern - ): { - preSelection: Array; - selectionClause: Cypher.Match | Cypher.With; - } { - let matchClause: Cypher.Match | Cypher.With = new Cypher.Match(node); + public transpile(context: QueryASTContext): OperationTranspileResult { + if (!context.target) throw new Error(); + + // eslint-disable-next-line prefer-const + let { selection: selectionClause, nestedContext } = this.selection.apply(context); - let extraMatches = this.getChildren().flatMap((f) => { - return f.getSelection(context); + let extraMatches: Array = this.getChildren().flatMap((f) => { + return f.getSelection(nestedContext); }); if (extraMatches.length > 0) { - extraMatches = [matchClause, ...extraMatches]; - matchClause = new Cypher.With("*"); + extraMatches = [selectionClause, ...extraMatches]; + selectionClause = new Cypher.With("*"); } - return { - preSelection: extraMatches, - selectionClause: matchClause, - }; - } - - private transpileNested(context: QueryASTContext): OperationTranspileResult { - if (!context.target || !this.relationship) throw new Error(); - const targetNode = createNodeFromEntity(this.target, context.neo4jGraphQLContext); - const relationship = new Cypher.Relationship({ type: this.relationship.type }); - const relDirection = this.relationship.getCypherDirection(this.directed); - - const pattern = new Cypher.Pattern(context.target) - .withoutLabels() - .related(relationship) - .withDirection(relDirection) - .to(targetNode); - - const nestedContext = context.push({ target: targetNode, relationship }); - - const { preSelection, selectionClause: clause } = this.getSelectionClauses(nestedContext, pattern); - const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) => - new Cypher.Call(sq).innerWith(targetNode) + new Cypher.Call(sq).innerWith(nestedContext.target) ); - - const nodeProjectionSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.nodeFields, [targetNode]); - - const predicates = this.filters.map((f) => f.getPredicate(nestedContext)); - const authPredicate = this.getAuthFilterPredicate(nestedContext); - const filters = Cypher.and(...predicates, ...authPredicate); - - const nodeProjectionMap = new Cypher.Map(); - this.nodeFields - .map((f) => f.getProjectionField(targetNode)) - .forEach((p) => { - if (typeof p === "string") { - nodeProjectionMap.set(p, targetNode.property(p)); - } else { - nodeProjectionMap.set(p); - } - }); - - if (nodeProjectionMap.size === 0) { - const targetNodeName = this.target.name; - nodeProjectionMap.set({ - __resolveType: new Cypher.Literal(targetNodeName), - __id: Cypher.id(targetNode), - }); - } - - const edgeVar = new Cypher.NamedVariable("edge"); const edgesVar = new Cypher.NamedVariable("edges"); const totalCount = new Cypher.NamedVariable("totalCount"); + const edgesProjectionVar = new Cypher.Variable(); - const edgeProjectionMap = new Cypher.Map(); - - this.edgeFields - .map((f) => f.getProjectionField(relationship)) - .forEach((p) => { - if (typeof p === "string") { - edgeProjectionMap.set(p, relationship.property(p)); - } else { - edgeProjectionMap.set(p); - } - }); - - edgeProjectionMap.set("node", nodeProjectionMap); - - let withWhere: Cypher.Clause | undefined; - if (filters) { - if (authFilterSubqueries.length > 0) { - // This is to avoid unnecessary With * - withWhere = new Cypher.With("*").where(filters); - } else { - clause.where(filters); - } - } + const unwindAndProjectionSubquery = this.createUnwindAndProjectionSubquery( + nestedContext, + edgesVar, + edgesProjectionVar + ); - let sortSubquery: Cypher.With | undefined; - if (this.pagination || this.sortFields.length > 0) { - const paginationField = this.pagination && this.pagination.getPagination(); + let withWhere: Cypher.With | undefined; - sortSubquery = this.getPaginationSubquery(nestedContext, edgesVar, paginationField); - sortSubquery.addColumns(totalCount); + if (authFilterSubqueries.length > 0) { + withWhere = new Cypher.With("*"); + this.addFiltersToClause(withWhere, nestedContext); + } else { + this.addFiltersToClause(selectionClause, nestedContext); } - let extraWithOrder: Cypher.Clause | undefined; - if (this.sortFields.length > 0) { - const sortFields = this.getSortFields({ - context: nestedContext, - nodeVar: targetNode, - edgeVar: relationship, - }); + const nodeAndRelationshipMap = new Cypher.Map({ + node: nestedContext.target, + }); - extraWithOrder = new Cypher.With(relationship, targetNode).orderBy(...sortFields); + if (nestedContext.relationship) { + nodeAndRelationshipMap.set("relationship", nestedContext.relationship); } - const projectionClauses = new Cypher.With([edgeProjectionMap, edgeVar]) - .with([Cypher.collect(edgeVar), edgesVar]) - .with(edgesVar, [Cypher.size(edgesVar), totalCount]); + const withCollectEdgesAndTotalCount = new Cypher.With([Cypher.collect(nodeAndRelationshipMap), edgesVar]).with( + edgesVar, + [Cypher.size(edgesVar), totalCount] + ); const returnClause = new Cypher.Return([ new Cypher.Map({ - edges: edgesVar, + edges: edgesProjectionVar, totalCount: totalCount, }), context.returnVariable, ]); - const subClause = Cypher.concat( - ...preSelection, - clause, - ...authFilterSubqueries, - withWhere, - extraWithOrder, - ...nodeProjectionSubqueries, - projectionClauses, - sortSubquery, - returnClause - ); return { - clauses: [subClause], + clauses: [ + Cypher.concat( + ...extraMatches, + selectionClause, + ...authFilterSubqueries, + withWhere, + withCollectEdgesAndTotalCount, + unwindAndProjectionSubquery, + returnClause + ), + ], projectionExpr: context.returnVariable, }; } - public transpile(context: QueryASTContext): OperationTranspileResult { - if (this.relationship) { - return this.transpileNested(context); - } - if (!hasTarget(context)) { - throw new Error("No parent node found!"); - } - - const targetNode = createNodeFromEntity(this.target, context.neo4jGraphQLContext, this.nodeAlias); + protected getAuthFilterSubqueries(context: QueryASTContext): Cypher.Clause[] { + return this.authFilters.flatMap((f) => f.getSubqueries(context)); + } - const { preSelection, selectionClause } = this.getSelectionClauses(context, targetNode); + protected getAuthFilterPredicate(context: QueryASTContext): Cypher.Predicate[] { + return filterTruthy(this.authFilters.map((f) => f.getPredicate(context))); + } + private createUnwindAndProjectionSubquery( + context: QueryASTContext, + edgesVar: Cypher.Variable, + returnVar: Cypher.Variable + ) { + const edgeVar = new Cypher.NamedVariable("edge"); const { prePaginationSubqueries, postPaginationSubqueries } = this.getPreAndPostSubqueries(context); - const authFilterSubqueries = this.getAuthFilterSubqueries(context).map((sq) => - new Cypher.Call(sq).innerWith(targetNode) - ); - - const authPredicate = this.getAuthFilterPredicate(context); - - const predicates = this.filters.map((f) => f.getPredicate(context)); - const filters = Cypher.and(...predicates, ...authPredicate); + let unwindClause: Cypher.With; + if (context.relationship) { + unwindClause = new Cypher.Unwind([edgesVar, edgeVar]).with( + [edgeVar.property("node"), context.target], + [edgeVar.property("relationship"), context.relationship] + ); + } else { + unwindClause = new Cypher.Unwind([edgesVar, edgeVar]).with([edgeVar.property("node"), context.target]); + } - const nodeProjectionMap = this.getProjectionMap(context); + const edgeProjectionMap = this.createProjectionMapForEdge(context); - const edgeVar = new Cypher.NamedVariable("edge"); - const edgesVar = new Cypher.NamedVariable("edges"); - const totalCount = new Cypher.NamedVariable("totalCount"); + const paginationWith = this.generateSortAndPaginationClause(context); - const edgeProjectionMap = new Cypher.Map(); - - edgeProjectionMap.set("node", nodeProjectionMap); + return new Cypher.Call( + Cypher.concat( + unwindClause, + ...prePaginationSubqueries, + paginationWith, + ...postPaginationSubqueries, + new Cypher.Return([Cypher.collect(edgeProjectionMap), returnVar]) + ) + ).innerWith(edgesVar); + } - let withWhere: Cypher.Clause | undefined; - if (filters) { - if (authFilterSubqueries.length > 0) { - // This is to avoid unnecessary With * - withWhere = new Cypher.With("*").where(filters); - } else { - selectionClause.where(filters); - } + private createProjectionMapForEdge(context: QueryASTContext): Cypher.Map { + const nodeProjectionMap = this.generateProjectionMapForFields(this.nodeFields, context.target); + if (nodeProjectionMap.size === 0) { + const targetNodeName = this.target.name; + nodeProjectionMap.set({ + __resolveType: new Cypher.Literal(targetNodeName), + __id: Cypher.id(context.target), + }); } - const withNodeAndTotalCount = new Cypher.With([Cypher.collect(targetNode), edgesVar]).with(edgesVar, [ - Cypher.size(edgesVar), - totalCount, - ]); - const unwindClause = new Cypher.Unwind([edgesVar, targetNode]).with(targetNode, totalCount); - let paginationWith: Cypher.With | undefined; - if (this.pagination || this.sortFields.length > 0) { - paginationWith = new Cypher.With("*"); - const paginationField = this.pagination && this.pagination.getPagination(); - if (paginationField?.limit) { - paginationWith.limit(paginationField.limit); - } - if (paginationField?.skip) { - paginationWith.skip(paginationField.skip); - } - if (this.sortFields.length > 0) { - const sortFields = this.getSortFields({ context, nodeVar: targetNode, aliased: true }); - paginationWith.orderBy(...sortFields); - } - } + let edgeProjectionMap = new Cypher.Map(); - const withProjection = new Cypher.With([edgeProjectionMap, edgeVar], totalCount, targetNode).with( - [Cypher.collect(edgeVar), edgesVar], - totalCount - ); + if (context.relationship) { + edgeProjectionMap = this.generateProjectionMapForFields(this.edgeFields, context.relationship); + } - let extraWithOrder: Cypher.Clause | undefined; + edgeProjectionMap.set("node", nodeProjectionMap); + return edgeProjectionMap; + } - const returnClause = new Cypher.Return([ - new Cypher.Map({ - edges: edgesVar, - totalCount: totalCount, - }), - context.returnVariable, - ]); + private generateProjectionMapForFields(fields: Field[], target: Cypher.Variable): Cypher.Map { + const projectionMap = new Cypher.Map(); + fields + .map((f) => f.getProjectionField(target)) + .forEach((p) => { + if (typeof p === "string") { + projectionMap.set(p, target.property(p)); + } else { + projectionMap.set(p); + } + }); - const connectionMatchAndAuthClause = Cypher.concat( - ...preSelection, - selectionClause, - ...authFilterSubqueries, - withWhere, - extraWithOrder, - withNodeAndTotalCount, - unwindClause - ); + return projectionMap; + } - const clause = Cypher.concat( - ...[ - connectionMatchAndAuthClause, - ...prePaginationSubqueries, - paginationWith, - ...postPaginationSubqueries, - withProjection, - returnClause, - ] - ); + private generateSortAndPaginationClause(context: QueryASTContext): Cypher.With | undefined { + const shouldGenerateSortWith = this.pagination || this.sortFields.length > 0; + if (!shouldGenerateSortWith) { + return undefined; + } + const paginationWith = new Cypher.With("*"); + this.addPaginationSubclauses(paginationWith); + this.addSortSubclause(paginationWith, context); - return { - clauses: [clause], - projectionExpr: context.returnVariable, - }; + return paginationWith; } - protected getPaginationSubquery( - context: QueryASTContext, - edgesVar: Cypher.Variable, - paginationField: PaginationField | undefined - ): Cypher.With { - const edgeVar = new Cypher.NamedVariable("edge"); - const subquery = new Cypher.Unwind([edgesVar, edgeVar]).with(edgeVar); - if (this.sortFields.length > 0) { - const sortFields = this.getSortFields({ context, nodeVar: edgeVar.property("node"), edgeVar }); - subquery.orderBy(...sortFields); + private addPaginationSubclauses(clause: Cypher.With): void { + const paginationField = this.pagination && this.pagination.getPagination(); + if (paginationField?.limit) { + clause.limit(paginationField.limit); } - - if (paginationField && paginationField.skip) { - subquery.skip(paginationField.skip); + if (paginationField?.skip) { + clause.skip(paginationField.skip); } + } - if (paginationField && paginationField.limit) { - subquery.limit(paginationField.limit); + private addSortSubclause(clause: Cypher.With, context: QueryASTContext): void { + if (this.sortFields.length > 0) { + const sortFields = this.getSortFields({ + context: context, + nodeVar: context.target, + edgeVar: context.relationship, + }); + clause.orderBy(...sortFields); } + } - const returnVar = new Cypher.Variable(); - subquery.return([Cypher.collect(edgeVar), returnVar]); - - return new Cypher.Call(subquery).innerWith(edgesVar).with([returnVar, edgesVar]); + private addFiltersToClause( + clause: Cypher.With | Cypher.Match | Cypher.Yield, + context: QueryASTContext + ): void { + const predicates = this.filters.map((f) => f.getPredicate(context)); + const authPredicate = this.getAuthFilterPredicate(context); + const predicate = Cypher.and(...predicates, ...authPredicate); + if (predicate) { + clause.where(predicate); + } } - protected getSortFields({ + private getSortFields({ context, nodeVar, edgeVar, - aliased = false, }: { context: QueryASTContext; nodeVar: Cypher.Variable | Cypher.Property; edgeVar?: Cypher.Variable | Cypher.Property; - aliased?: boolean; }): SortField[] { + const aliasSort = true; return this.sortFields.flatMap(({ node, edge }) => { - const nodeFields = node.flatMap((s) => s.getSortFields(context, nodeVar, aliased)); + const nodeFields = node.flatMap((s) => s.getSortFields(context, nodeVar, aliasSort)); if (edgeVar) { - const edgeFields = edge.flatMap((s) => s.getSortFields(context, edgeVar, aliased)); + const edgeFields = edge.flatMap((s) => s.getSortFields(context, edgeVar, aliasSort)); return [...nodeFields, ...edgeFields]; } return nodeFields; }); } - protected getPreAndPostSubqueries(context: QueryASTContext): { + private getPreAndPostSubqueries(context: QueryASTContext): { prePaginationSubqueries: Cypher.Clause[]; postPaginationSubqueries: Cypher.Clause[]; } { @@ -445,26 +359,4 @@ export class ConnectionReadOperation extends Operation { postPaginationSubqueries: postNodeSubqueries, }; } - - protected getProjectionMap(context: QueryASTContext): Cypher.MapProjection { - if (!hasTarget(context)) throw new Error("No parent node found!"); - const projectionFields = this.nodeFields.map((f) => f.getProjectionField(context.target)); - const sortProjectionFields = this.sortFields.flatMap((f) => - f.node.map((sortNode) => sortNode.getProjectionField(context)) - ); - - const uniqueProjectionFields = Array.from(new Set([...projectionFields, ...sortProjectionFields])); // TODO remove duplicates with alias - - const stringFields: string[] = []; - let otherFields: Record = {}; - - for (const field of uniqueProjectionFields) { - if (typeof field === "string") stringFields.push(field); - else { - otherFields = { ...otherFields, ...field }; - } - } - - return new Cypher.MapProjection(context.target, stringFields, otherFields); - } } diff --git a/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts index 681edb2fce8..ed63e1a147c 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import type Cypher from "@neo4j/cypher-builder"; +import Cypher from "@neo4j/cypher-builder"; import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { filterTruthy } from "../../../../utils/utils"; @@ -26,6 +26,7 @@ import type { QueryASTNode } from "../QueryASTNode"; import type { FulltextScoreField } from "../fields/FulltextScoreField"; import type { EntitySelection } from "../selection/EntitySelection"; import { ReadOperation } from "./ReadOperation"; +import type { OperationTranspileResult } from "./operations"; export type FulltextOptions = { index: string; @@ -59,6 +60,24 @@ export class FulltextOperation extends ReadOperation { this.scoreField = scoreField; } + public transpile(context: QueryASTContext): OperationTranspileResult { + const { clauses, projectionExpr } = super.transpile(context); + + const extraProjectionColumns: Array<[Cypher.Expr, Cypher.Variable]> = []; + + if (this.scoreField) { + const scoreProjection = this.scoreField.getProjectionField(context.returnVariable); + + extraProjectionColumns.push([scoreProjection.score, new Cypher.NamedVariable("score")]); + } + + return { + clauses, + projectionExpr, + extraProjectionColumns, + }; + } + public getChildren(): QueryASTNode[] { return filterTruthy([...super.getChildren(), this.scoreField]); } diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts index e862c760dd3..c0365cea9ce 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts @@ -106,41 +106,48 @@ export class ReadOperation extends Operation { ): OperationTranspileResult { const isCreateSelection = context.env.topLevelOperationName === "CREATE"; //TODO: dupe from transpile - if (!hasTarget(context)) throw new Error("No parent node found!"); + if (!hasTarget(context)) { + throw new Error("No parent node found!"); + } // eslint-disable-next-line prefer-const let { selection: matchClause, nestedContext } = this.selection.apply(context); + const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.filters, [nestedContext.target]); const filterPredicates = this.getPredicates(nestedContext); + const fieldSubqueries = this.getFieldsSubqueries(nestedContext); + const cypherFieldSubqueries = this.getCypherFieldsSubqueries(nestedContext); + const subqueries = Cypher.concat(...fieldSubqueries, ...cypherFieldSubqueries); + const sortSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.sortFields, [nestedContext.target]); const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) => new Cypher.Call(sq).innerWith(nestedContext.target) ); + const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext); + const ret = this.getProjectionClause(nestedContext, context.returnVariable, entity.isList); let extraMatches: SelectionClause[] = this.getChildren().flatMap((f) => { return f.getSelection(nestedContext); }); - if (extraMatches.length > 0) { extraMatches = [matchClause, ...extraMatches]; matchClause = new Cypher.With("*"); } - const wherePredicate = Cypher.and(filterPredicates, ...authFiltersPredicate); - let withWhere: Cypher.With | undefined; - let filterSubqueryWith: Cypher.With | undefined; - + let filterSubqueriesClause: Cypher.Clause | undefined = undefined; // This weird condition is just for cypher compatibility - const shouldAddWithForAuth = authFiltersPredicate.length > 0; - if (authFilterSubqueries.length > 0 || shouldAddWithForAuth) { + const shouldAddWithForAuth = authFilterSubqueries.length > 0 || authFiltersPredicate.length > 0; + if (filterSubqueries.length > 0 || shouldAddWithForAuth) { if (!isCreateSelection || authFilterSubqueries.length) { + filterSubqueriesClause = Cypher.concat(...filterSubqueries); // for creates auth filters sometimes use variables from the subquery filterSubqueryWith = new Cypher.With("*"); } } + const wherePredicate = Cypher.and(filterPredicates, ...authFiltersPredicate); if (wherePredicate) { if (filterSubqueryWith) { filterSubqueryWith.where(wherePredicate); // TODO: should this only be for aggregation filters? @@ -149,17 +156,12 @@ export class ReadOperation extends Operation { } } - const cypherFieldSubqueries = this.getCypherFieldsSubqueries(nestedContext); - const subqueries = Cypher.concat(...this.getFieldsSubqueries(nestedContext), ...cypherFieldSubqueries); - const sortSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.sortFields, [nestedContext.target]); - const ret = this.getProjectionClause(nestedContext, context.returnVariable, entity.isList); - const clause = Cypher.concat( ...extraMatches, matchClause, ...authFilterSubqueries, + filterSubqueriesClause, filterSubqueryWith, - withWhere, subqueries, ...sortSubqueries, ret @@ -196,36 +198,13 @@ export class ReadOperation extends Operation { return Cypher.and(...this.filters.map((f) => f.getPredicate(queryASTContext))); } - protected getSelectionClauses( - context: QueryASTContext, - node: Cypher.Node | Cypher.Pattern - ): { - preSelection: Array; - selectionClause: Cypher.Match | Cypher.With | Cypher.Yield; - } { - let matchClause: Cypher.Match | Cypher.With = new Cypher.Match(node); - - let extraMatches = this.getChildren().flatMap((f) => { - return f.getSelection(context); - }); - - if (extraMatches.length > 0) { - extraMatches = [matchClause, ...extraMatches]; - matchClause = new Cypher.With("*"); - } - - return { - preSelection: extraMatches, - selectionClause: matchClause, - }; - } - public transpile(context: QueryASTContext): OperationTranspileResult { if (this.relationship) { return this.transpileNestedRelationship(this.relationship, context); } let { selection: matchClause, nestedContext } = this.selection.apply(context); + const isCreateSelection = nestedContext.env.topLevelOperationName === "CREATE"; if (isCreateSelection) { if (!context.hasTarget()) { @@ -240,14 +219,14 @@ export class ReadOperation extends Operation { const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.filters, [nestedContext.target]); const filterPredicates = this.getPredicates(nestedContext); + const fieldSubqueries = this.getFieldsSubqueries(nestedContext); + const cypherFieldSubqueries = this.getCypherFieldsSubqueries(nestedContext); + const subqueries = Cypher.concat(...fieldSubqueries); + const sortSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.sortFields, [nestedContext.target]); const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) => new Cypher.Call(sq).innerWith(nestedContext.target) ); - const fieldSubqueries = this.getFieldsSubqueries(nestedContext); - const cypherFieldSubqueries = this.getCypherFieldsSubqueries(nestedContext); - const sortSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.sortFields, [nestedContext.target]); - const subqueries = Cypher.concat(...fieldSubqueries); const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext); const ret: Cypher.Return = this.getReturnStatement( @@ -258,14 +237,13 @@ export class ReadOperation extends Operation { let extraMatches: SelectionClause[] = this.getChildren().flatMap((f) => { return f.getSelection(nestedContext); }); - if (extraMatches.length > 0) { extraMatches = [matchClause, ...extraMatches]; matchClause = new Cypher.With("*"); } + let filterSubqueryWith: Cypher.With | undefined; let filterSubqueriesClause: Cypher.Clause | undefined = undefined; - // This weird condition is just for cypher compatibility const shouldAddWithForAuth = authFiltersPredicate.length > 0; if (filterSubqueries.length > 0 || shouldAddWithForAuth) { @@ -299,7 +277,7 @@ export class ReadOperation extends Operation { : Cypher.concat(sortBlock, ...cypherFieldSubqueries); let clause: Cypher.Clause; - // Top-level read part of a mutation does not contains the MATCH clause as is implicit in the mutation. + // Top-level read part of a mutation does not contain the MATCH clause as it is implicit in the mutation. if (isCreateSelection) { clause = Cypher.concat(filterSubqueriesClause, filterSubqueryWith, sortAndLimitBlock, subqueries, ret); } else { diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionPartial.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionPartial.ts index 57466a68683..4f6de7dba2c 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionPartial.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionPartial.ts @@ -18,30 +18,28 @@ */ import Cypher from "@neo4j/cypher-builder"; -import { createNodeFromEntity } from "../../../utils/create-node-from-entity"; -import { ConnectionReadOperation } from "../ConnectionReadOperation"; -import type { OperationTranspileResult } from "../operations"; -import type { Sort } from "../../sort/Sort"; -import type { Pagination } from "../../pagination/Pagination"; import { wrapSubqueriesInCypherCalls } from "../../../utils/wrap-subquery-in-calls"; import type { QueryASTContext } from "../../QueryASTContext"; +import type { Pagination } from "../../pagination/Pagination"; +import { ConnectionReadOperation } from "../ConnectionReadOperation"; +import type { OperationTranspileResult } from "../operations"; export class CompositeConnectionPartial extends ConnectionReadOperation { public transpile(context: QueryASTContext): OperationTranspileResult { if (!context.target) throw new Error(); if (!this.relationship) throw new Error("connection fields are not supported on top level interface"); - const node = createNodeFromEntity(this.target, context.neo4jGraphQLContext); - const relationship = new Cypher.Relationship({ type: this.relationship.type }); - const relDirection = this.relationship.getCypherDirection(this.directed); - const pattern = new Cypher.Pattern(context.target) - .withoutLabels() - .related(relationship) - .withDirection(relDirection) - .to(node); + // eslint-disable-next-line prefer-const + let { selection: clause, nestedContext } = this.selection.apply(context); - const nestedContext = context.push({ target: node, relationship }); - const { preSelection, selectionClause: clause } = this.getSelectionClauses(nestedContext, pattern); + let extraMatches: Array = this.getChildren().flatMap((f) => { + return f.getSelection(nestedContext); + }); + + if (extraMatches.length > 0) { + extraMatches = [clause, ...extraMatches]; + clause = new Cypher.With("*"); + } const predicates = this.filters.map((f) => f.getPredicate(nestedContext)); @@ -51,17 +49,19 @@ export class CompositeConnectionPartial extends ConnectionReadOperation { const filters = Cypher.and(...predicates, ...authPredicate); - const nodeProjectionSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.nodeFields, [node]); + const nodeProjectionSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.nodeFields, [ + nestedContext.target, + ]); const nodeProjectionMap = new Cypher.Map(); // This bit is different than normal connection ops const targetNodeName = this.target.name; nodeProjectionMap.set({ __resolveType: new Cypher.Literal(targetNodeName), - __id: Cypher.id(node), + __id: Cypher.id(nestedContext.target), }); - const nodeProjectionFields = this.nodeFields.map((f) => f.getProjectionField(node)); + const nodeProjectionFields = this.nodeFields.map((f) => f.getProjectionField(nestedContext.target)); const nodeSortProjectionFields = this.sortFields.flatMap((f) => f.node.map((ef) => ef.getProjectionField(nestedContext)) ); @@ -70,7 +70,7 @@ export class CompositeConnectionPartial extends ConnectionReadOperation { uniqueNodeProjectionFields.forEach((p) => { if (typeof p === "string") { - nodeProjectionMap.set(p, node.property(p)); + nodeProjectionMap.set(p, nestedContext.target.property(p)); } else { nodeProjectionMap.set(p); } @@ -80,7 +80,7 @@ export class CompositeConnectionPartial extends ConnectionReadOperation { const edgeProjectionMap = new Cypher.Map(); - const edgeProjectionFields = this.edgeFields.map((f) => f.getProjectionField(relationship)); + const edgeProjectionFields = this.edgeFields.map((f) => f.getProjectionField(nestedContext.relationship!)); const edgeSortProjectionFields = this.sortFields.flatMap((f) => f.edge.map((ef) => ef.getProjectionField(nestedContext)) ); @@ -89,7 +89,7 @@ export class CompositeConnectionPartial extends ConnectionReadOperation { uniqueEdgeProjectionFields.forEach((p) => { if (typeof p === "string") { - edgeProjectionMap.set(p, relationship.property(p)); + edgeProjectionMap.set(p, nestedContext.relationship!.property(p)); } else { edgeProjectionMap.set(p); } @@ -110,7 +110,8 @@ export class CompositeConnectionPartial extends ConnectionReadOperation { const projectionClauses = new Cypher.With([edgeProjectionMap, edgeVar]).return(context.returnVariable); const subClause = Cypher.concat( - ...preSelection, + // ...preSelection, + ...extraMatches, clause, ...authFilterSubqueries, withWhere, @@ -124,11 +125,6 @@ export class CompositeConnectionPartial extends ConnectionReadOperation { }; } - // Sort is handled by CompositeConnectionReadOperation - public addSort(sortElement: { node: Sort[]; edge: Sort[] }): void { - this.sortFields.push(sortElement); - } - // Pagination is handled by CompositeConnectionReadOperation public addPagination(_pagination: Pagination): void { return undefined; diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionReadOperation.ts index 8888b8e3e5d..edc11653e6a 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeConnectionReadOperation.ts @@ -21,13 +21,13 @@ import Cypher from "@neo4j/cypher-builder"; import type { OperationTranspileResult } from "../operations"; import { Operation } from "../operations"; -import type { QueryASTNode } from "../../QueryASTNode"; -import type { CompositeConnectionPartial } from "./CompositeConnectionPartial"; -import type { Sort, SortField } from "../../sort/Sort"; -import type { Pagination } from "../../pagination/Pagination"; -import { QueryASTContext } from "../../QueryASTContext"; import { filterTruthy } from "../../../../../utils/utils"; import { hasTarget } from "../../../utils/context-has-target"; +import { QueryASTContext } from "../../QueryASTContext"; +import type { QueryASTNode } from "../../QueryASTNode"; +import type { Pagination } from "../../pagination/Pagination"; +import type { Sort, SortField } from "../../sort/Sort"; +import type { CompositeConnectionPartial } from "./CompositeConnectionPartial"; export class CompositeConnectionReadOperation extends Operation { private children: CompositeConnectionPartial[]; diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts index ca9656ef2a4..7326bd3e15d 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadOperation.ts @@ -18,16 +18,16 @@ */ import Cypher from "@neo4j/cypher-builder"; -import type { QueryASTNode } from "../../QueryASTNode"; -import type { OperationTranspileResult } from "../operations"; -import { Operation } from "../operations"; -import type { CompositeReadPartial } from "./CompositeReadPartial"; -import type { UnionEntityAdapter } from "../../../../../schema-model/entity/model-adapters/UnionEntityAdapter"; import type { InterfaceEntityAdapter } from "../../../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; +import type { UnionEntityAdapter } from "../../../../../schema-model/entity/model-adapters/UnionEntityAdapter"; import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import type { QueryASTContext } from "../../QueryASTContext"; +import type { QueryASTNode } from "../../QueryASTNode"; import type { Pagination } from "../../pagination/Pagination"; import type { Sort, SortField } from "../../sort/Sort"; -import type { QueryASTContext } from "../../QueryASTContext"; +import type { OperationTranspileResult } from "../operations"; +import { Operation } from "../operations"; +import type { CompositeReadPartial } from "./CompositeReadPartial"; export class CompositeReadOperation extends Operation { private children: CompositeReadPartial[]; @@ -55,37 +55,7 @@ export class CompositeReadOperation extends Operation { return this.children; } - private transpileTopLevelCompositeRead(context: QueryASTContext): OperationTranspileResult { - const nestedSubqueries = this.children.flatMap((c) => { - const result = c.transpile(context); - return result.clauses; - }); - const nestedSubquery = new Cypher.Call(new Cypher.Union(...nestedSubqueries)).return(context.returnVariable); - if (this.sortFields.length > 0) { - nestedSubquery.orderBy(...this.getSortFields(context, context.returnVariable)); - } - if (this.pagination) { - const paginationField = this.pagination.getPagination(); - if (paginationField) { - if (paginationField.skip) { - nestedSubquery.skip(paginationField.skip); - } - if (paginationField.limit) { - nestedSubquery.limit(paginationField.limit); - } - } - } - return { - clauses: [nestedSubquery], - projectionExpr: context.returnVariable, - }; - } - public transpile(context: QueryASTContext): OperationTranspileResult { - if (!this.relationship) { - return this.transpileTopLevelCompositeRead(context); - } - const parentNode = context.target; const nestedSubqueries = this.children.flatMap((c) => { const result = c.transpile(context); @@ -98,6 +68,9 @@ export class CompositeReadOperation extends Operation { }); let aggrExpr: Cypher.Expr = Cypher.collect(context.returnVariable); + if (!this.relationship) { + aggrExpr = context.returnVariable; + } if (this.relationship && !this.relationship.isList) { aggrExpr = Cypher.head(aggrExpr); } diff --git a/packages/graphql/src/translate/queryAST/ast/operations/operations.ts b/packages/graphql/src/translate/queryAST/ast/operations/operations.ts index 2350634eb94..616f0f2e0e3 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/operations.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/operations.ts @@ -18,12 +18,13 @@ */ import type Cypher from "@neo4j/cypher-builder"; -import { QueryASTNode } from "../QueryASTNode"; import type { QueryASTContext } from "../QueryASTContext"; +import { QueryASTNode } from "../QueryASTNode"; export type OperationTranspileResult = { projectionExpr: Cypher.Expr; clauses: Cypher.Clause[]; + extraProjectionColumns?: Array<[Cypher.Expr, Cypher.Variable]>; // This embeds extra columns in the last return, used as a hack for fulltext score }; export abstract class Operation extends QueryASTNode { diff --git a/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts index df06591b7ec..7afbf0f8a92 100644 --- a/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts +++ b/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts @@ -28,7 +28,8 @@ export abstract class EntitySelection extends QueryASTNode { return []; } - /** Apply selection over the given context, returns the updated context and the selection clause */ + /** Apply selection over the given context, returns the updated context and the selection clause + * TODO: Improve naming */ public abstract apply(context: QueryASTContext): { nestedContext: QueryASTContext; selection: SelectionClause; diff --git a/packages/graphql/src/translate/queryAST/ast/sort/CypherPropertySort.ts b/packages/graphql/src/translate/queryAST/ast/sort/CypherPropertySort.ts index 0d6bbffec72..62bbce8303d 100644 --- a/packages/graphql/src/translate/queryAST/ast/sort/CypherPropertySort.ts +++ b/packages/graphql/src/translate/queryAST/ast/sort/CypherPropertySort.ts @@ -18,12 +18,12 @@ */ import type Cypher from "@neo4j/cypher-builder"; -import type { SortField } from "./Sort"; -import { Sort } from "./Sort"; import type { AttributeAdapter } from "../../../../schema-model/attribute/model-adapters/AttributeAdapter"; -import type { QueryASTNode } from "../QueryASTNode"; -import type { QueryASTContext } from "../QueryASTContext"; import { CypherAnnotationSubqueryGenerator } from "../../cypher-generators/CypherAnnotationSubqueryGenerator"; +import type { QueryASTContext } from "../QueryASTContext"; +import type { QueryASTNode } from "../QueryASTNode"; +import type { SortField } from "./Sort"; +import { Sort } from "./Sort"; export class CypherPropertySort extends Sort { private attribute: AttributeAdapter; @@ -49,17 +49,9 @@ export class CypherPropertySort extends Sort { public getSortFields( context: QueryASTContext, - variable: Cypher.Variable | Cypher.Property, - sortByDatabaseName = true + _variable: Cypher.Variable | Cypher.Property, + _sortByDatabaseName = true ): SortField[] { - const isNested = context.source; - if (isNested) { - const attributeName = sortByDatabaseName ? this.attribute.databaseName : this.attribute.name; - - const nodeProperty = variable.property(attributeName); - return [[nodeProperty, this.direction]]; - } - const projectionVar = context.getScopeVariable(this.attribute.name); return [[projectionVar, this.direction]]; } diff --git a/packages/graphql/src/translate/queryAST/cypher-generators/CypherAnnotationSubqueryGenerator.ts b/packages/graphql/src/translate/queryAST/cypher-generators/CypherAnnotationSubqueryGenerator.ts index dfce0fc18a1..540bd268dac 100644 --- a/packages/graphql/src/translate/queryAST/cypher-generators/CypherAnnotationSubqueryGenerator.ts +++ b/packages/graphql/src/translate/queryAST/cypher-generators/CypherAnnotationSubqueryGenerator.ts @@ -123,7 +123,7 @@ export class CypherAnnotationSubqueryGenerator { const target = this.context.target; const aliasTargetToPublicTarget = new Cypher.With([target, CYPHER_TARGET_VARIABLE]); - const statementCypherQuery = new Cypher.RawCypher((env) => { + const statementCypherQuery = new Cypher.Raw((env) => { const statement = this.replaceArgumentsInStatement({ env, rawArguments, diff --git a/packages/graphql/src/translate/queryAST/factory/AuthFilterFactory.ts b/packages/graphql/src/translate/queryAST/factory/AuthFilterFactory.ts index 08c697c9250..d2eeb79f67f 100644 --- a/packages/graphql/src/translate/queryAST/factory/AuthFilterFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/AuthFilterFactory.ts @@ -17,6 +17,7 @@ * limitations under the License. */ +import { asArray } from "@graphql-tools/utils"; import Cypher from "@neo4j/cypher-builder"; import type { AttributeAdapter } from "../../../schema-model/attribute/model-adapters/AttributeAdapter"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; @@ -27,18 +28,17 @@ import type { AuthorizationOperation } from "../../../types/authorization"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { isLogicalOperator } from "../../utils/logical-operators"; import type { RelationshipWhereOperator, WhereOperator } from "../../where/types"; +import type { ConnectionFilter } from "../ast/filters/ConnectionFilter"; import type { Filter } from "../ast/filters/Filter"; import { LogicalFilter } from "../ast/filters/LogicalFilter"; import type { RelationshipFilter } from "../ast/filters/RelationshipFilter"; +import { AuthConnectionFilter } from "../ast/filters/authorization-filters/AuthConnectionFilter"; import { AuthRelationshipFilter } from "../ast/filters/authorization-filters/AuthRelationshipFilter"; import { JWTFilter } from "../ast/filters/authorization-filters/JWTFilter"; import { ParamPropertyFilter } from "../ast/filters/property-filters/ParamPropertyFilter"; import { PropertyFilter } from "../ast/filters/property-filters/PropertyFilter"; import { FilterFactory } from "./FilterFactory"; import { parseWhereField } from "./parsers/parse-where-field"; -import type { ConnectionFilter } from "../ast/filters/ConnectionFilter"; -import { AuthConnectionFilter } from "../ast/filters/authorization-filters/AuthConnectionFilter"; -import { asArray } from "@graphql-tools/utils"; export class AuthFilterFactory extends FilterFactory { // PopulatedWhere has the values as Cypher variables @@ -188,6 +188,7 @@ export class AuthFilterFactory extends FilterFactory { protected createRelationshipFilterTreeNode(options: { relationship: RelationshipAdapter; + target: ConcreteEntityAdapter | InterfaceEntityAdapter; isNot: boolean; operator: RelationshipWhereOperator; }): RelationshipFilter { diff --git a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts index 9bc4e7bda2e..8e66f5bc05f 100644 --- a/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/FilterFactory.ts @@ -39,6 +39,7 @@ import { CountFilter } from "../ast/filters/aggregation/CountFilter"; import { DurationFilter } from "../ast/filters/property-filters/DurationFilter"; import { PointFilter } from "../ast/filters/property-filters/PointFilter"; import { PropertyFilter } from "../ast/filters/property-filters/PropertyFilter"; +import { TypenameFilter } from "../ast/filters/property-filters/TypenameFilter"; import { getConcreteEntities } from "../utils/get-concrete-entities"; import { isConcreteEntity } from "../utils/is-concrete-entity"; import { isInterfaceEntity } from "../utils/is-interface-entity"; @@ -57,11 +58,12 @@ type AggregateWhereInput = { }; export class FilterFactory { - private queryASTFactory: QueryASTFactory; + protected experimental: boolean; constructor(queryASTFactory: QueryASTFactory) { - this.queryASTFactory = queryASTFactory; + this.experimental = queryASTFactory.experimental; } + /** * Get all the entities explicitly required by the where "on" object. If it's a concrete entity it will return itself. **/ @@ -187,10 +189,10 @@ export class FilterFactory { } private createRelationshipFilter( - where: GraphQLWhereArg, relationship: RelationshipAdapter, + where: GraphQLWhereArg, filterOps: { isNot: boolean; operator: RelationshipWhereOperator | undefined } - ): RelationshipFilter | undefined { + ): RelationshipFilter[] { /** * The logic below can be confusing, but it's to handle the following cases: * 1. where: { actors: null } -> in this case we want to return an Exists filter as showed by tests packages/graphql/tests/tck/null.test.ts @@ -198,35 +200,60 @@ export class FilterFactory { **/ const isNull = where === null; if (!isNull && Object.keys(where).length === 0) { - return; + return []; } - // this is because if isNull it's true we want to wrap the Exist subclause in a NOT, but if it's isNull it's true and isNot it's true they negate each other + // this is because if isNull is true we want to wrap the Exist subclause in a NOT, but if isNull is true and isNot is true they negate each other const isNot = isNull ? !filterOps.isNot : filterOps.isNot; - const relationshipFilter = this.createRelationshipFilterTreeNode({ - relationship: relationship, - isNot, - operator: filterOps.operator || "SOME", - }); + if (isInterfaceEntity(relationship.target) && !where.node?._on) { + const relationshipFilter = this.createRelationshipFilterTreeNode({ + relationship, + target: relationship.target, + isNot, + operator: filterOps.operator || "SOME", + }); - if (!isNull) { - const targetNode = relationship.target; - const targetNodeFilters = this.createNodeFilters(targetNode, where); - relationshipFilter.addTargetNodeFilter(...targetNodeFilters); - } + if (!isNull) { + const targetNode = relationship.target; + const targetNodeFilters = this.createNodeFilters(targetNode, where); + relationshipFilter.addTargetNodeFilter(...targetNodeFilters); + } + + return [relationshipFilter]; + } else { + const filteredEntities = this.filterConcreteEntities(relationship.target, where); + const relationshipFilters: RelationshipFilter[] = []; + for (const concreteEntity of filteredEntities) { + const relationshipFilter = this.createRelationshipFilterTreeNode({ + relationship, + target: concreteEntity, + isNot, + operator: filterOps.operator || "SOME", + }); + + if (!isNull) { + const entityWhere = where[concreteEntity.name] ?? where; + const targetNodeFilters = this.createNodeFilters(concreteEntity, entityWhere); + relationshipFilter.addTargetNodeFilter(...targetNodeFilters); + } - return relationshipFilter; + relationshipFilters.push(relationshipFilter); + } + return relationshipFilters; + } } - // This allow to override this creation in AuthorizationFilterFactory + // This allows to override this creation in AuthorizationFilterFactory protected createRelationshipFilterTreeNode(options: { relationship: RelationshipAdapter; + target: ConcreteEntityAdapter | InterfaceEntityAdapter; isNot: boolean; operator: RelationshipWhereOperator; }): RelationshipFilter { return new RelationshipFilter(options); } - // This allow to override this creation in AuthorizationFilterFactory + + // This allows to override this creation in AuthorizationFilterFactory protected createConnectionFilterTreeNode(options: { relationship: RelationshipAdapter; target: ConcreteEntityAdapter | InterfaceEntityAdapter; @@ -235,20 +262,102 @@ export class FilterFactory { }): ConnectionFilter { return new ConnectionFilter(options); } + /** + * By removing the _on field is possible to create a single filter to be applied to all the concrete entities. + * */ + private createExperimentalInterfaceFilters(entity: InterfaceEntityAdapter, where: Record): Filter[] { + const filters = filterTruthy( + Object.entries(where).flatMap(([key, value]): Filter | undefined => { + if (isLogicalOperator(key)) { + const nestedFilters = asArray(value).flatMap((nestedWhere) => { + return this.createExperimentalInterfaceFilters(entity, nestedWhere); + }); + return new LogicalFilter({ + operation: key, + filters: nestedFilters, + }); + } + if (key === "typename_IN") { + const acceptedEntities = entity.concreteEntities.filter((ce) => { + return asArray(value).some((v) => v === ce.name); + }); + const typenameFilter = new TypenameFilter(acceptedEntities); + return typenameFilter; + } + + const { fieldName, operator, isNot } = parseWhereField(key); + + const attr = entity.findAttribute(fieldName); + + if (fieldName === "id" && !attr && !isUnionEntity(entity)) { + const relayAttribute = entity.globalIdField; + if (relayAttribute) { + const relayIdData = fromGlobalId(value as string); + if (relayIdData) { + const { typeName, field } = relayIdData; + let id = relayIdData.id; + + if (typeName !== entity.name || !field || !id) { + throw new Error(`Cannot query Relay Id on "${entity.name}"`); + } + const idAttribute = entity.findAttribute(field); + if (!idAttribute) throw new Error(`Attribute ${field} not found`); + + if (idAttribute.typeHelper.isNumeric()) { + id = Number(id); + if (Number.isNaN(id)) { + throw new Error("Can't parse non-numeric relay id"); + } + } + return this.createPropertyFilter({ + attribute: idAttribute, + comparisonValue: id, + isNot, + operator, + }); + } + } + } + + if (!attr) throw new Error(`Attribute ${fieldName} not found`); + return this.createPropertyFilter({ + attribute: attr, + comparisonValue: value, + isNot, + operator, + }); + }) + ); + + return this.wrapMultipleFiltersInLogical(filters); + } + // TODO: remove _on implementation logic from this method when _on will be completely deprecated. // TODO: rename and refactor this, createNodeFilters is misleading for non-connection operations public createNodeFilters(entity: EntityAdapter, where: Record): Filter[] { + // if typename is allowed we can compute only the shared filter without recomputing the filters for each concrete entity + // if typename filters are allowed we are getting rid of the _on and the implicit typename filter. + const typenameFilterAllowed = this.experimental && isInterfaceEntity(entity); + if (typenameFilterAllowed) { + return this.createExperimentalInterfaceFilters(entity, where); + } const whereFields = this.getConcreteFiltersWhere(entity, where); - const filters = filterTruthy( - Object.entries(whereFields).flatMap(([key, value]): Filter | undefined => { + Object.entries(whereFields).flatMap(([key, value]): Filter | Filter[] | undefined => { + if (isLogicalOperator(key)) { + const nestedFilters = asArray(value).flatMap((nestedWhere) => { + return this.createNodeFilters(entity, nestedWhere); + }); + return new LogicalFilter({ + operation: key, + filters: nestedFilters, + }); + } + if (key === "_on" && isObject(value)) { return this.getConcreteFilter(entity, value); } - if (isLogicalOperator(key)) { - return this.createNodeLogicalFilter(key, value, entity); - } const { fieldName, operator, isNot, isConnection, isAggregate } = parseWhereField(key); let relationship: RelationshipAdapter | undefined; @@ -257,7 +366,9 @@ export class FilterFactory { } if (isConnection) { - if (!relationship) throw new Error(`Relationship not found for connection ${fieldName}`); + if (!relationship) { + throw new Error(`Relationship not found for connection ${fieldName}`); + } if (operator && !isRelationshipOperator(operator)) { throw new Error(`Invalid operator ${operator} for relationship`); } @@ -269,9 +380,11 @@ export class FilterFactory { return this.wrapMultipleFiltersInLogical(connectionFilters)[0]; } if (isAggregate) { - if (!relationship) throw new Error(`Relationship not found for connection ${fieldName}`); + if (!relationship) { + throw new Error(`Relationship not found for connection ${fieldName}`); + } - return this.createAggregationFilter(value as AggregateWhereInput, relationship); + return this.createAggregationFilter(relationship, value as AggregateWhereInput); } if (relationship) { @@ -279,7 +392,7 @@ export class FilterFactory { throw new Error(`Invalid operator ${operator} for relationship`); } - return this.createRelationshipFilter(value as GraphQLWhereArg, relationship, { + return this.createRelationshipFilter(relationship, value as GraphQLWhereArg, { isNot, operator, }); @@ -299,7 +412,9 @@ export class FilterFactory { throw new Error(`Cannot query Relay Id on "${entity.name}"`); } const idAttribute = entity.findAttribute(field); - if (!idAttribute) throw new Error(`Attribute ${field} not found`); + if (!idAttribute) { + throw new Error(`Attribute ${field} not found`); + } if (idAttribute.typeHelper.isNumeric()) { id = Number(id); @@ -317,7 +432,9 @@ export class FilterFactory { } } - if (!attr) throw new Error(`Attribute ${fieldName} not found`); + if (!attr) { + throw new Error(`Attribute ${fieldName} not found`); + } return this.createPropertyFilter({ attribute: attr, comparisonValue: value, @@ -333,7 +450,13 @@ export class FilterFactory { public createEdgeFilters(relationship: RelationshipAdapter, where: GraphQLWhereArg): Filter[] { const filterASTs = Object.entries(where).map(([key, value]): Filter => { if (isLogicalOperator(key)) { - return this.createEdgeLogicalFilter(key, value, relationship); + const nestedFilters = asArray(value).flatMap((nestedWhere) => { + return this.createEdgeFilters(relationship, nestedWhere); + }); + return new LogicalFilter({ + operation: key, + filters: nestedFilters, + }); } const { fieldName, operator, isNot } = parseWhereField(key); const attribute = relationship.findAttribute(fieldName); @@ -351,34 +474,6 @@ export class FilterFactory { return this.wrapMultipleFiltersInLogical(filterTruthy(filterASTs)); } - private createNodeLogicalFilter( - operation: "OR" | "AND" | "NOT", - where: GraphQLWhereArg[] | GraphQLWhereArg, - entity: EntityAdapter - ): LogicalFilter { - const nestedFilters = asArray(where).flatMap((nestedWhere) => { - return this.createNodeFilters(entity, nestedWhere); - }); - return new LogicalFilter({ - operation, - filters: nestedFilters, - }); - } - - private createEdgeLogicalFilter( - operation: "OR" | "AND" | "NOT", - where: GraphQLWhereArg[] | GraphQLWhereArg, - relationship: RelationshipAdapter - ): LogicalFilter { - const nestedFilters = asArray(where).flatMap((nestedWhere) => { - return this.createEdgeFilters(relationship, nestedWhere); - }); - return new LogicalFilter({ - operation, - filters: nestedFilters, - }); - } - private getAggregationNestedFilters( where: AggregateWhereInput, relationship: RelationshipAdapter @@ -426,7 +521,7 @@ export class FilterFactory { return this.wrapMultipleFiltersInLogical(nestedFilters); } - private createAggregationFilter(where: AggregateWhereInput, relationship: RelationshipAdapter): AggregationFilter { + private createAggregationFilter(relationship: RelationshipAdapter, where: AggregateWhereInput): AggregationFilter { const aggregationFilter = new AggregationFilter(relationship); const nestedFilters = this.getAggregationNestedFilters(where, relationship); aggregationFilter.addFilters(...nestedFilters); @@ -514,7 +609,7 @@ export class FilterFactory { for (const concreteEntity of concreteEntities) { const concreteEntityWhere: Record = where[concreteEntity.name]; if (concreteEntityWhere) { - const concreteEntityFilters = this.createNodeFilters(concreteEntity, concreteEntityWhere); + const concreteEntityFilters = this.createNodeFilters(entity, concreteEntityWhere); nodeFilters.push(...concreteEntityFilters); } } diff --git a/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts b/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts index 9e6a5abb308..29f6bfdc6d3 100644 --- a/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts @@ -33,6 +33,7 @@ import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphq import { filterTruthy, isObject, isString } from "../../../utils/utils"; import { checkEntityAuthentication } from "../../authorization/check-authentication"; import { FulltextScoreField } from "../ast/fields/FulltextScoreField"; +import type { Filter } from "../ast/filters/Filter"; import type { AuthorizationFilters } from "../ast/filters/authorization-filters/AuthorizationFilters"; import { FulltextScoreFilter } from "../ast/filters/property-filters/FulltextScoreFilter"; import { AggregationOperation } from "../ast/operations/AggregationOperation"; @@ -73,12 +74,14 @@ export class OperationsFactory { private fieldFactory: FieldFactory; private sortAndPaginationFactory: SortAndPaginationFactory; private authorizationFactory: AuthorizationFactory; + private experimental: boolean; constructor(queryASTFactory: QueryASTFactory) { this.filterFactory = queryASTFactory.filterFactory; this.fieldFactory = queryASTFactory.fieldFactory; this.sortAndPaginationFactory = queryASTFactory.sortAndPaginationFactory; this.authorizationFactory = queryASTFactory.authorizationFactory; + this.experimental = queryASTFactory.experimental; } public createTopLevelOperation( @@ -281,7 +284,18 @@ export class OperationsFactory { whereArgs: resolveTreeWhere, }); } else { - const concreteEntities = getConcreteEntitiesInOnArgumentOfWhere(entity, resolveTreeWhere); + // if typename is allowed and therefore _on is disabled we can compute only the shared filter without recomputing the filters for each concrete entity + // if typename filters are allowed we are getting rid of the _on and the implicit typename filter. + const typenameFilterAllowed = this.experimental && isInterfaceEntity(entity); + + const concreteEntities = typenameFilterAllowed + ? entity.concreteEntities + : getConcreteEntitiesInOnArgumentOfWhere(entity, resolveTreeWhere); + + const sharedFilters = typenameFilterAllowed + ? this.filterFactory.createNodeFilters(entity, resolveTreeWhere) + : undefined; + const concreteReadOperations = concreteEntities.map((concreteEntity: ConcreteEntityAdapter) => { // Duplicate from normal read let selection: EntitySelection; @@ -313,6 +327,7 @@ export class OperationsFactory { resolveTree, context, whereArgs: whereArgs, + sharedFilters, }); }); @@ -332,7 +347,6 @@ export class OperationsFactory { resolveTree: ResolveTree, context: Neo4jGraphQLTranslationContext ): AggregationOperation | CompositeAggregationOperation { - console.log("createAggregationOperation"); let entity: ConcreteEntityAdapter | InterfaceEntityAdapter; if (entityOrRel instanceof RelationshipAdapter) { entity = entityOrRel.target as ConcreteEntityAdapter; @@ -541,10 +555,17 @@ export class OperationsFactory { const concreteEntities = getConcreteEntitiesInOnArgumentOfWhere(target, nodeWhere); const concreteConnectionOperations = concreteEntities.map((concreteEntity: ConcreteEntityAdapter) => { + const selection = new RelationshipSelection({ + relationship, + directed, + targetOverride: concreteEntity, + }); + const connectionPartial = new CompositeConnectionPartial({ relationship, directed, target: concreteEntity, + selection, }); return this.hydrateConnectionOperationAST({ @@ -586,7 +607,19 @@ export class OperationsFactory { targetOperations: ["READ"], context, }); - const operation = new ConnectionReadOperation({ relationship, directed, target }); + + let selection: EntitySelection; + if (relationship) { + selection = new RelationshipSelection({ + relationship, + directed: Boolean(resolveTree.args?.directed ?? true), + }); + } else { + selection = new NodeSelection({ + target, + }); + } + const operation = new ConnectionReadOperation({ relationship, directed, target, selection }); return this.hydrateConnectionOperationAST({ relationship: relationship, @@ -836,6 +869,7 @@ export class OperationsFactory { context, sortArgs, fieldsByTypeName, + sharedFilters, }: { entity: ConcreteEntityAdapter; operation: T; @@ -843,6 +877,7 @@ export class OperationsFactory { whereArgs: Record; sortArgs?: Record; fieldsByTypeName: FieldsByTypeName; + sharedFilters?: Filter[]; }): T { const concreteProjectionFields = { ...fieldsByTypeName[entity.name] }; // Get the abstract types of the interface @@ -856,7 +891,7 @@ export class OperationsFactory { ]); const fields = this.fieldFactory.createFields(entity, projectionFields, context); - const filters = this.filterFactory.createNodeFilters(entity, whereArgs); + const filters = sharedFilters ? sharedFilters : this.filterFactory.createNodeFilters(entity, whereArgs); const authFilters = this.authorizationFactory.createEntityAuthFilters(entity, ["READ"], context); const authValidate = this.authorizationFactory.createEntityAuthValidate(entity, ["READ"], context, "BEFORE"); @@ -910,12 +945,14 @@ export class OperationsFactory { resolveTree, context, whereArgs, + sharedFilters, }: { entity: ConcreteEntityAdapter; operation: T; resolveTree: ResolveTree; context: Neo4jGraphQLTranslationContext; - whereArgs: Record; + whereArgs: Record | Filter[]; + sharedFilters?: Filter[]; }): T { return this.hydrateOperation({ entity, @@ -924,6 +961,7 @@ export class OperationsFactory { whereArgs, fieldsByTypeName: resolveTree.fieldsByTypeName, sortArgs: (resolveTree.args.options as Record) || {}, + sharedFilters, }); } @@ -971,8 +1009,7 @@ export class OperationsFactory { context, "BEFORE" ); - - const filters = this.filterFactory.createNodeFilters(entity, whereArgs); // Aggregation filters only apply to target node + const filters = this.filterFactory.createNodeFilters(entity, whereArgs); operation.setFields(fields); operation.setNodeFields(nodeFields); diff --git a/packages/graphql/src/translate/queryAST/factory/QueryASTFactory.ts b/packages/graphql/src/translate/queryAST/factory/QueryASTFactory.ts index 003c041723b..be1ab8a4801 100644 --- a/packages/graphql/src/translate/queryAST/factory/QueryASTFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/QueryASTFactory.ts @@ -37,9 +37,11 @@ export class QueryASTFactory { public fieldFactory: FieldFactory; public sortAndPaginationFactory: SortAndPaginationFactory; public authorizationFactory: AuthorizationFactory; + public experimental: boolean; - constructor(schemaModel: Neo4jGraphQLSchemaModel) { + constructor(schemaModel: Neo4jGraphQLSchemaModel, experimental: boolean) { this.schemaModel = schemaModel; + this.experimental = experimental; this.filterFactory = new FilterFactory(this); this.fieldFactory = new FieldFactory(this); this.sortAndPaginationFactory = new SortAndPaginationFactory(); diff --git a/packages/graphql/src/translate/queryAST/utils/get-concrete-where.ts b/packages/graphql/src/translate/queryAST/utils/get-concrete-where.ts index ec98969f749..b1ac33035f4 100644 --- a/packages/graphql/src/translate/queryAST/utils/get-concrete-where.ts +++ b/packages/graphql/src/translate/queryAST/utils/get-concrete-where.ts @@ -48,7 +48,9 @@ export function getConcreteWhere( return whereArgs[concreteTarget.name] ?? {}; } else { // interface may have shared filters, inject them as if they were present under _on - const sharedInterfaceFilters = Object.entries(whereArgs).filter(([key]) => key !== "_on"); + const sharedInterfaceFilters = Object.entries(whereArgs).filter( + ([key]) => key !== "_on" && key !== "typename_IN" + ); const _on: Record | undefined = isObject(whereArgs["_on"]) ? whereArgs["_on"] : undefined; // if concrete target is present in _on then merge its filters with the shared ones, if _on it's not defined then returns the shared filters diff --git a/packages/graphql/src/translate/translate-aggregate.ts b/packages/graphql/src/translate/translate-aggregate.ts index de879f03377..44a147e6b67 100644 --- a/packages/graphql/src/translate/translate-aggregate.ts +++ b/packages/graphql/src/translate/translate-aggregate.ts @@ -19,7 +19,6 @@ import type Cypher from "@neo4j/cypher-builder"; import Debug from "debug"; -import type { Node } from "../classes"; import { DEBUG_TRANSLATE } from "../constants"; import type { EntityAdapter } from "../schema-model/entity/EntityAdapter"; import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; @@ -27,7 +26,7 @@ import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; const debug = Debug(DEBUG_TRANSLATE); -function translateQuery({ +export function translateAggregate({ context, entityAdapter, }: { @@ -36,7 +35,7 @@ function translateQuery({ }): Cypher.CypherResult { const { resolveTree } = context; // TODO: Rename QueryAST to OperationsTree - const queryASTFactory = new QueryASTFactory(context.schemaModel); + const queryASTFactory = new QueryASTFactory(context.schemaModel, context.experimental); if (!entityAdapter) throw new Error("Entity not found"); const queryAST = queryASTFactory.createQueryAST(resolveTree, entityAdapter, context); @@ -44,18 +43,3 @@ function translateQuery({ const clause = queryAST.buildNew(context); return clause.build(); } - -function translateAggregate({ - node, - context, - entityAdapter, -}: { - node: Node | undefined; - context: Neo4jGraphQLTranslationContext; - entityAdapter: EntityAdapter; -}): Cypher.CypherResult { - // TODO: Move fulltext to new translation layer to remove the deprecated translation - return translateQuery({ context, entityAdapter }); -} - -export default translateAggregate; diff --git a/packages/graphql/src/translate/translate-create.ts b/packages/graphql/src/translate/translate-create.ts index 51ec0d2b5e3..8c28e6ff798 100644 --- a/packages/graphql/src/translate/translate-create.ts +++ b/packages/graphql/src/translate/translate-create.ts @@ -119,7 +119,7 @@ export default async function translateCreate({ throw new Error(`Transpilation error: ${node.name} is not a concrete entity`); } - const queryAST = new QueryASTFactory(context.schemaModel).createQueryAST( + const queryAST = new QueryASTFactory(context.schemaModel, context.experimental).createQueryAST( resolveTree, concreteEntityAdapter, context @@ -150,7 +150,7 @@ export default async function translateCreate({ ); const returnStatement = getReturnStatement(projectedVariables, context); - const createQuery = new Cypher.RawCypher((env) => { + const createQuery = new Cypher.Raw((env) => { const cypher = filterTruthy([ `${createStrs.join("\n")}`, context.subscriptionsEnabled ? `WITH ${projectionWith.join(", ")}` : "", diff --git a/packages/graphql/src/translate/translate-delete.ts b/packages/graphql/src/translate/translate-delete.ts index 198cfb766f4..7e91e278942 100644 --- a/packages/graphql/src/translate/translate-delete.ts +++ b/packages/graphql/src/translate/translate-delete.ts @@ -81,7 +81,7 @@ export function translateDelete({ deleteStr = findConnectedNodesCypherQuery(varName); } - const deleteQuery = new Cypher.RawCypher(() => { + const deleteQuery = new Cypher.Raw(() => { const eventMeta = createEventMeta({ event: "delete", nodeVariable: varName, typename: node.name }); const cypher = [ ...(context.subscriptionsEnabled ? [`WITH [] AS ${META_CYPHER_VARIABLE}`] : []), diff --git a/packages/graphql/src/translate/translate-read.ts b/packages/graphql/src/translate/translate-read.ts index f7bd764372b..6d78547f942 100644 --- a/packages/graphql/src/translate/translate-read.ts +++ b/packages/graphql/src/translate/translate-read.ts @@ -19,7 +19,6 @@ import type Cypher from "@neo4j/cypher-builder"; import Debug from "debug"; -import type { Node } from "../classes"; import { DEBUG_TRANSLATE } from "../constants"; import type { EntityAdapter } from "../schema-model/entity/EntityAdapter"; import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; @@ -29,20 +28,16 @@ const debug = Debug(DEBUG_TRANSLATE); export function translateRead( { - node, context, - isRootConnectionField, entityAdapter, }: { context: Neo4jGraphQLTranslationContext; - node?: Node; - isRootConnectionField?: boolean; entityAdapter: EntityAdapter; }, varName = "this" ): Cypher.CypherResult { const { resolveTree } = context; - const operationsTreeFactory = new QueryASTFactory(context.schemaModel); + const operationsTreeFactory = new QueryASTFactory(context.schemaModel, context.experimental); if (!entityAdapter) throw new Error("Entity not found"); const operationsTree = operationsTreeFactory.createQueryAST(resolveTree, entityAdapter, context); diff --git a/packages/graphql/src/translate/translate-resolve-reference.ts b/packages/graphql/src/translate/translate-resolve-reference.ts index e9815579075..a0d3fd7e716 100644 --- a/packages/graphql/src/translate/translate-resolve-reference.ts +++ b/packages/graphql/src/translate/translate-resolve-reference.ts @@ -72,7 +72,7 @@ export function translateResolveReference({ const projectionSubqueries = Cypher.concat(...projection.subqueries, ...projection.subqueriesBeforeSort); - const projectionExpression = new Cypher.RawCypher((env) => { + const projectionExpression = new Cypher.Raw((env) => { return [`${varName} ${compileCypher(projection.projection, env)}`, projection.params]; }); diff --git a/packages/graphql/src/translate/translate-top-level-cypher.ts b/packages/graphql/src/translate/translate-top-level-cypher.ts index ade8b908dd2..55169a64c8d 100644 --- a/packages/graphql/src/translate/translate-top-level-cypher.ts +++ b/packages/graphql/src/translate/translate-top-level-cypher.ts @@ -114,7 +114,7 @@ export function translateTopLevelCypher({ const innerNodePartialProjection = `[ this IN [this] WHERE (${labelsStatements.join(" AND ")})`; if (!resolveTree.fieldsByTypeName[node.name]) { headStrs.push( - new Cypher.RawCypher(`${innerNodePartialProjection}| this { __resolveType: "${node.name}" }]`) + new Cypher.Raw(`${innerNodePartialProjection}| this { __resolveType: "${node.name}" }]`) ); } else { const { @@ -135,7 +135,7 @@ export function translateTopLevelCypher({ projectionValidatePredicates.push(...predicates); } headStrs.push( - new Cypher.RawCypher((env) => { + new Cypher.Raw((env) => { return innerNodePartialProjection .concat(`| this { __resolveType: "${node.name}", `) .concat(compileCypher(str, env).replace("{", "")) @@ -146,7 +146,7 @@ export function translateTopLevelCypher({ } }); - projectionStr = new Cypher.RawCypher( + projectionStr = new Cypher.Raw( (env) => `${headStrs.map((headStr) => compileCypher(headStr, env)).join(" + ")}` ); } @@ -192,7 +192,7 @@ export function translateTopLevelCypher({ const projectionSubquery = Cypher.concat(...projectionSubqueries); - return new Cypher.RawCypher((env) => { + return new Cypher.Raw((env) => { const authPredicates: Cypher.Predicate[] = []; if (projectionValidatePredicates.length) { diff --git a/packages/graphql/src/translate/translate-update.ts b/packages/graphql/src/translate/translate-update.ts index c580cc68990..53a490736d2 100644 --- a/packages/graphql/src/translate/translate-update.ts +++ b/packages/graphql/src/translate/translate-update.ts @@ -464,7 +464,7 @@ export default async function translateUpdate({ const relationshipValidationStr = createRelationshipValidationStr({ node, context, varName }); - const updateQuery = new Cypher.RawCypher((env: Cypher.Environment) => { + const updateQuery = new Cypher.Raw((env: Cypher.Environment) => { const projectionSubqueryStr = projectionSubquery ? compileCypher(projectionSubquery, env) : ""; const cypher = [ @@ -524,21 +524,19 @@ function generateUpdateReturnStatement( ): Cypher.Clause { let statements; if (varName && projStr) { - statements = new Cypher.RawCypher( - (env) => `collect(DISTINCT ${varName} ${compileCypher(projStr, env)}) AS data` - ); + statements = new Cypher.Raw((env) => `collect(DISTINCT ${varName} ${compileCypher(projStr, env)}) AS data`); } if (subscriptionsEnabled) { statements = Cypher.concat( statements, - new Cypher.RawCypher(statements ? ", " : ""), - new Cypher.RawCypher(`collect(DISTINCT m) as ${META_CYPHER_VARIABLE}`) + new Cypher.Raw(statements ? ", " : ""), + new Cypher.Raw(`collect(DISTINCT m) as ${META_CYPHER_VARIABLE}`) ); } if (!statements) { - statements = new Cypher.RawCypher("'Query cannot conclude with CALL'"); + statements = new Cypher.Raw("'Query cannot conclude with CALL'"); } return new Cypher.Return(statements); diff --git a/packages/graphql/src/translate/unwind-create.ts b/packages/graphql/src/translate/unwind-create.ts index 7127ab4892b..8a10e216afe 100644 --- a/packages/graphql/src/translate/unwind-create.ts +++ b/packages/graphql/src/translate/unwind-create.ts @@ -60,7 +60,7 @@ export default async function unwindCreate({ throw new Error(`Transpilation error: ${node.name} is not a concrete entity`); } - const queryAST = new QueryASTFactory(context.schemaModel).createQueryAST( + const queryAST = new QueryASTFactory(context.schemaModel, context.experimental).createQueryAST( resolveTree, concreteEntityAdapter, context diff --git a/packages/graphql/src/translate/utils/callback-utils.ts b/packages/graphql/src/translate/utils/callback-utils.ts index 7ff885976ed..60d5a5d7f85 100644 --- a/packages/graphql/src/translate/utils/callback-utils.ts +++ b/packages/graphql/src/translate/utils/callback-utils.ts @@ -52,13 +52,13 @@ export const addCallbackAndSetParamCypher = ( callbackBucket: CallbackBucket, operation: "CREATE" | "UPDATE", node: Cypher.Node -): [Cypher.Property, Cypher.RawCypher] | [] => { +): [Cypher.Property, Cypher.Raw] | [] => { if (!field.callback || !field.callback.operations.includes(operation)) { return []; } const propRef = node.property(field.dbPropertyName as string); - const rawCypherStatement = new Cypher.RawCypher((env: Cypher.Environment) => { + const rawCypherStatement = new Cypher.Raw((env: Cypher.Environment) => { const variableCypher = compileCypher(variable, env); const paramName = `${variableCypher}_${field.fieldName}_${field.callback?.callbackName}`; diff --git a/packages/graphql/src/translate/utils/stringify-object.ts b/packages/graphql/src/translate/utils/stringify-object.ts index 771f2db5047..925ce8f0415 100644 --- a/packages/graphql/src/translate/utils/stringify-object.ts +++ b/packages/graphql/src/translate/utils/stringify-object.ts @@ -20,15 +20,13 @@ import Cypher from "@neo4j/cypher-builder"; /** Serializes object into a string for Cypher objects */ -export function stringifyObject( - fields: Record -): Cypher.RawCypher { - return new Cypher.RawCypher( +export function stringifyObject(fields: Record): Cypher.Raw { + return new Cypher.Raw( (env) => `{ ${Object.entries(fields) .filter(([, value]) => Boolean(value)) .map(([key, value]): string | undefined => { - if (value instanceof Cypher.RawCypher) { + if (value instanceof Cypher.Raw) { return `${key}: ${value?.getCypher(env)}`; } else { return `${key}: ${value}`; diff --git a/packages/graphql/src/translate/where/create-connection-where-and-params.ts b/packages/graphql/src/translate/where/create-connection-where-and-params.ts index 23fca8ba264..20cde103bf2 100644 --- a/packages/graphql/src/translate/where/create-connection-where-and-params.ts +++ b/packages/graphql/src/translate/where/create-connection-where-and-params.ts @@ -54,7 +54,7 @@ export default function createConnectionWhereAndParams({ }); let subquery = ""; - const whereCypher = new Cypher.RawCypher((env: Cypher.Environment) => { + const whereCypher = new Cypher.Raw((env: Cypher.Environment) => { const cypher = (andOp as any)?.getCypher(env) || ""; if (preComputedSubqueries) { subquery = compileCypher(preComputedSubqueries, env); diff --git a/packages/graphql/src/translate/where/create-where-and-params.ts b/packages/graphql/src/translate/where/create-where-and-params.ts index 3a559c46fb2..10438d8b5c5 100644 --- a/packages/graphql/src/translate/where/create-where-and-params.ts +++ b/packages/graphql/src/translate/where/create-where-and-params.ts @@ -53,7 +53,7 @@ export default function createWhereAndParams({ let preComputedWhereFieldsResult = ""; - const whereCypher = new Cypher.RawCypher((env: Cypher.Environment) => { + const whereCypher = new Cypher.Raw((env: Cypher.Environment) => { preComputedWhereFieldsResult = compileCypherIfExists(preComputedSubqueries, env); const cypher = (wherePredicate as any)?.getCypher(env) || ""; return [cypher, {}]; diff --git a/packages/graphql/src/translate/where/create-where-predicate.ts b/packages/graphql/src/translate/where/create-where-predicate.ts index d48ed0ff7c4..640f2e6e810 100644 --- a/packages/graphql/src/translate/where/create-where-predicate.ts +++ b/packages/graphql/src/translate/where/create-where-predicate.ts @@ -73,7 +73,7 @@ export function createWhereNodePredicate({ predicate: Cypher.Predicate | undefined; preComputedSubqueries?: Cypher.CompositeClause | undefined; } { - const factory = new QueryASTFactory(context.schemaModel); + const factory = new QueryASTFactory(context.schemaModel, context.experimental); const queryASTEnv = new QueryASTEnv(); const queryASTContext = new QueryASTContext({ @@ -100,7 +100,7 @@ export function createWhereEdgePredicate({ predicate: Cypher.Predicate | undefined; preComputedSubqueries?: Cypher.CompositeClause | undefined; } { - const factory = new QueryASTFactory(context.schemaModel); + const factory = new QueryASTFactory(context.schemaModel, context.experimental); const queryASTEnv = new QueryASTEnv(); const queryASTContext = new QueryASTContext({ diff --git a/packages/graphql/src/types/index.ts b/packages/graphql/src/types/index.ts index 07eec2daf0e..352731284c7 100644 --- a/packages/graphql/src/types/index.ts +++ b/packages/graphql/src/types/index.ts @@ -113,8 +113,6 @@ export interface BaseField { arguments: InputValueDefinitionNode[]; private?: boolean; description?: string; - readonly?: boolean; - writeonly?: boolean; dbPropertyName?: string; dbPropertyNameUnescaped?: string; unique?: Unique; diff --git a/packages/graphql/tests/integration/connections/sort.int.test.ts b/packages/graphql/tests/integration/connections/sort.int.test.ts index 76582e34d5d..a436b82df12 100644 --- a/packages/graphql/tests/integration/connections/sort.int.test.ts +++ b/packages/graphql/tests/integration/connections/sort.int.test.ts @@ -17,14 +17,14 @@ * limitations under the License. */ -import type { Driver, Session } from "neo4j-driver"; import type { GraphQLSchema } from "graphql"; import { graphql } from "graphql"; -import { generate } from "randomstring"; import { gql } from "graphql-tag"; -import Neo4j from "../neo4j"; +import type { Driver, Session } from "neo4j-driver"; +import { generate } from "randomstring"; import { Neo4jGraphQL } from "../../../src/classes"; import { UniqueType } from "../../utils/graphql-types"; +import Neo4j from "../neo4j"; const testLabel = generate({ charset: "alphabetic" }); diff --git a/packages/graphql/tests/integration/experimental/aggegations/filter-aggregation-interfaces-field-level.int.test.ts b/packages/graphql/tests/integration/experimental/aggegations/filter-aggregation-interfaces-field-level.int.test.ts index 9fd2176c30c..75b1620b232 100644 --- a/packages/graphql/tests/integration/experimental/aggegations/filter-aggregation-interfaces-field-level.int.test.ts +++ b/packages/graphql/tests/integration/experimental/aggegations/filter-aggregation-interfaces-field-level.int.test.ts @@ -192,4 +192,68 @@ describe("Field-level filter interface query fields", () => { }, ]); }); + + test("complex query on nested aggregation with logical operator", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + actedInAggregate(where: { AND: [{title_STARTS_WITH: "The"}, {NOT: {title_CONTAINS: "Series"}}] }) { + edge { + screenTime { + min + max + } + } + node { + title { + longest + shortest + } + } + } + name, + } + } + `; + + const token = createBearerToken(secret, {}); + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect((queryResult as any).data[Actor.plural]).toIncludeSameMembers([ + { + actedInAggregate: { + edge: { + screenTime: { + max: 100, + min: 20, + }, + }, + node: { + title: { + longest: "The Movie Three", + shortest: "The Movie One", + }, + }, + }, + name: "Actor One", + }, + { + actedInAggregate: { + edge: { + screenTime: { + max: 728, + min: 240, + }, + }, + node: { + title: { + longest: "The Movie Three", + shortest: "The Movie Two", + }, + }, + }, + name: "Actor Two", + }, + ]); + }); }); diff --git a/packages/graphql/tests/integration/experimental/aggegations/filter-aggregation-interfaces-top-level.int.test.ts b/packages/graphql/tests/integration/experimental/aggegations/filter-aggregation-interfaces-top-level.int.test.ts index f284917224b..d6753071db4 100644 --- a/packages/graphql/tests/integration/experimental/aggegations/filter-aggregation-interfaces-top-level.int.test.ts +++ b/packages/graphql/tests/integration/experimental/aggegations/filter-aggregation-interfaces-top-level.int.test.ts @@ -116,6 +116,25 @@ describe("Top-level filter interface query fields", () => { }); }); + test("top level count with logical operator", async () => { + const query = ` + query { + productionsAggregate(where: { OR: [{title: "The Show"}, {title: "A Movie"}] }) { + count + } + } + `; + + const token = createBearerToken(secret, {}); + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + productionsAggregate: { + count: 2, + }, + }); + }); + test("top level count and string fields", async () => { const query = ` query { diff --git a/packages/graphql/tests/integration/experimental/interface-filtering.int.test.ts b/packages/graphql/tests/integration/experimental/interface-filtering.int.test.ts new file mode 100644 index 00000000000..7f61a5b6771 --- /dev/null +++ b/packages/graphql/tests/integration/experimental/interface-filtering.int.test.ts @@ -0,0 +1,182 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { GraphQLSchema } from "graphql"; +import { graphql } from "graphql"; +import type { Driver } from "neo4j-driver"; +import { Neo4jGraphQL } from "../../../src"; +import { cleanNodes } from "../../utils/clean-nodes"; +import { createBearerToken } from "../../utils/create-bearer-token"; +import { UniqueType } from "../../utils/graphql-types"; +import Neo4j from "../neo4j"; + +describe("Interface filtering", () => { + const secret = "the-secret"; + + let schema: GraphQLSchema; + let neo4j: Neo4j; + let driver: Driver; + let typeDefs: string; + + const Movie = new UniqueType("Movie"); + const Series = new UniqueType("Series"); + const Actor = new UniqueType("Actor"); + + async function graphqlQuery(query: string, token: string) { + return graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues({ token }), + }); + } + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + + typeDefs = ` + interface Show { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type ${Movie} implements Show @limit(default: 3, max: 10) { + title: String! + cost: Float + runtime: Int + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type ${Series} implements Show { + title: String! + episodes: Int + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type ${Actor} { + name: String! + actedIn: [Show!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + interface ActedIn @relationshipProperties { + screenTime: Int + } + `; + + const session = await neo4j.getSession(); + + try { + await session.run(` + CREATE(m:${Movie} { title: "The Office" }) + CREATE(m2:${Movie}{ title: "The Office 2" }) + CREATE(m3:${Movie}{ title: "NOT The Office 2" }) + CREATE(s1:${Series}{ title: "The Office 2" }) + CREATE(s2:${Series}{ title: "NOT The Office" }) + CREATE(a:${Actor} {name: "Keanu"}) + MERGE(a)-[:ACTED_IN]->(m) + MERGE(a)-[:ACTED_IN]->(m2) + MERGE(a)-[:ACTED_IN]->(m3) + MERGE(a)-[:ACTED_IN]->(s1) + MERGE(a)-[:ACTED_IN]->(s2) + + `); + } finally { + await session.close(); + } + + const neoGraphql = new Neo4jGraphQL({ + typeDefs, + driver, + features: { + authorization: { + key: secret, + }, + }, + experimental: true, + }); + schema = await neoGraphql.getSchema(); + }); + + afterAll(async () => { + const session = await neo4j.getSession(); + await cleanNodes(session, [Movie, Series, Actor]); + await session.close(); + await driver.close(); + }); + + test("allow for logical filters on top-level interfaces", async () => { + const query = ` + query actedInWhere { + shows(where: { OR: [{ title: "The Office" }, { title: "The Office 2" }] }) { + title + } + } + `; + + const token = createBearerToken(secret, {}); + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + shows: expect.toIncludeSameMembers([ + { + title: "The Office", + }, + { + title: "The Office 2", + }, + { + title: "The Office 2", + }, + ]), + }); + }); + + test("allow for logical filters on nested-level interfaces", async () => { + const query = ` + query actedInWhere { + ${Actor.plural} { + actedIn(where: { OR: [{ title: "The Office" }, { title: "The Office 2" }] }) { + title + } + } + } + `; + + const token = createBearerToken(secret, {}); + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + [Actor.plural]: [ + { + actedIn: expect.toIncludeSameMembers([ + { + title: "The Office", + }, + { + title: "The Office 2", + }, + { + title: "The Office 2", + }, + ]), + }, + ], + }); + }); +}); diff --git a/packages/graphql/tests/integration/experimental/interfaces-top-level.int.test.ts b/packages/graphql/tests/integration/experimental/interfaces-top-level.int.test.ts index 499190b0954..a3153137cad 100644 --- a/packages/graphql/tests/integration/experimental/interfaces-top-level.int.test.ts +++ b/packages/graphql/tests/integration/experimental/interfaces-top-level.int.test.ts @@ -20,11 +20,11 @@ import type { GraphQLSchema } from "graphql"; import { graphql } from "graphql"; import type { Driver } from "neo4j-driver"; -import Neo4j from "../neo4j"; import { Neo4jGraphQL } from "../../../src"; -import { UniqueType } from "../../utils/graphql-types"; -import { createBearerToken } from "../../utils/create-bearer-token"; import { cleanNodes } from "../../utils/clean-nodes"; +import { createBearerToken } from "../../utils/create-bearer-token"; +import { UniqueType } from "../../utils/graphql-types"; +import Neo4j from "../neo4j"; describe("Top-level interface query fields", () => { const secret = "the-secret"; @@ -201,177 +201,6 @@ describe("Top-level interface query fields", () => { }); }); - test("should return results on top-level simple query on simple interface with filters", async () => { - const neoGraphql = new Neo4jGraphQL({ - typeDefs, - driver, - features: { - authorization: { - key: secret, - }, - }, - experimental: true, - }); - schema = await neoGraphql.getSchema(); - - const query = ` - query { - myOtherInterfaces(where: {_on:{ ${SomeNodeType}: { other: {id: "2"}} } }) { - id - ... on ${SomeNodeType} { - id - other { - id - } - } - } - } - `; - - const token = createBearerToken(secret, {}); - const queryResult = await graphqlQuery(query, token); - expect(queryResult.errors).toBeUndefined(); - expect(queryResult.data).toEqual({ - myOtherInterfaces: expect.toIncludeSameMembers([ - { - id: "1", - other: [ - { - id: "2", - }, - ], - }, - { - id: "10", - other: [ - { - id: "2", - }, - ], - }, - ]), - }); - }); - - test("should return results on top-level simple query on interface target to a relationship with filters", async () => { - const neoGraphql = new Neo4jGraphQL({ - typeDefs, - driver, - features: { - authorization: { - key: secret, - }, - }, - experimental: true, - }); - schema = await neoGraphql.getSchema(); - - const query = ` - query { - myInterfaces(where: { _on: { ${SomeNodeType}: {somethingElse_NOT: "test"}, ${MyOtherImplementationType}: {someField: "bla"} } }) { - id - ... on ${MyOtherImplementationType} { - someField - } - ... on MyOtherInterface { - something - ... on ${SomeNodeType} { - somethingElse - } - } - } - } - `; - - const token = createBearerToken(secret, {}); - const queryResult = await graphqlQuery(query, token); - expect(queryResult.errors).toBeUndefined(); - expect(queryResult.data).toEqual({ - myInterfaces: [ - { - id: "10", - something: "someothernode", - somethingElse: "othertest", - }, - { - id: "4", - someField: "bla", - }, - ], - }); - }); - - test("Type filtering using onType", async () => { - const neoGraphql = new Neo4jGraphQL({ - typeDefs, - driver, - features: { - authorization: { - key: secret, - }, - }, - experimental: true, - }); - schema = await neoGraphql.getSchema(); - - const query = ` - query { - myInterfaces(where: { _on: { ${MyOtherImplementationType}: {} } }) { - id - ... on ${MyOtherImplementationType} { - someField - } - - } - } - `; - - const token = createBearerToken(secret, {}); - const queryResult = await graphqlQuery(query, token); - expect(queryResult.errors).toBeUndefined(); - expect(queryResult.data).toEqual({ - myInterfaces: [ - { - id: "4", - someField: "bla", - }, - ], - }); - }); - - test("Filter overriding using onType", async () => { - const neoGraphql = new Neo4jGraphQL({ - typeDefs, - driver, - features: { - authorization: { - key: secret, - }, - }, - experimental: true, - }); - schema = await neoGraphql.getSchema(); - - const query = ` - query { - myInterfaces(where: { id_STARTS_WITH: "4", _on: { ${MyOtherImplementationType}: {id_STARTS_WITH: "1"} } }) { - id - ... on ${MyOtherImplementationType} { - someField - } - - } - } - `; - - const token = createBearerToken(secret, {}); - const queryResult = await graphqlQuery(query, token); - expect(queryResult.errors).toBeUndefined(); - expect(queryResult.data).toEqual({ - myInterfaces: [], - }); - }); - test("should return results on top-level simple query on simple interface sorted", async () => { const query = ` query { @@ -449,65 +278,6 @@ describe("Top-level interface query fields", () => { }); }); - test("should return results on top-level simple query on interface target to a relationship sorted", async () => { - const query = ` - query { - myInterfaces(where: { _on: { ${SomeNodeType}: {somethingElse_NOT: "test"}, ${MyOtherImplementationType}: {} } }, options: {sort: [{id: ASC}]}) { - id - ... on ${MyOtherImplementationType} { - someField - } - ... on MyOtherInterface { - something - ... on ${SomeNodeType} { - somethingElse - other(options: { sort: [{id: DESC}] }) { - id - } - } - } - } - } - `; - - const session = await neo4j.getSession(); - - try { - await session.run(` - MATCH (s:${SomeNodeType} { id: "10", something:"someothernode",somethingElse:"othertest" }) - CREATE (other:${OtherNodeType} { id: "30" }) - MERGE (s)-[:HAS_OTHER_NODES]->(other) - `); - } finally { - await session.close(); - } - - const token = createBearerToken(secret, {}); - const queryResult = await graphqlQuery(query, token); - expect(queryResult.errors).toBeUndefined(); - expect(queryResult.data).toEqual({ - myInterfaces: [ - { - id: "10", - something: "someothernode", - somethingElse: "othertest", - other: [ - { - id: "30", - }, - { - id: "2", - }, - ], - }, - { - id: "4", - someField: "bla", - }, - ], - }); - }); - describe("add authorization", () => { beforeAll(async () => { typeDefs = @@ -659,49 +429,6 @@ describe("Top-level interface query fields", () => { ]), }); }); - - test("should combine filters with authorization filters", async () => { - const neoGraphql = new Neo4jGraphQL({ - typeDefs, - driver, - features: { - authorization: { - key: secret, - }, - }, - experimental: true, - }); - schema = await neoGraphql.getSchema(); - - const query = ` - query { - myInterfaces(where: { _on: { ${SomeNodeType}: {somethingElse_NOT: "test"}, ${MyOtherImplementationType}: {someField: "bla"} } }) { - id - ... on ${MyOtherImplementationType} { - someField - } - ... on MyOtherInterface { - something - ... on ${SomeNodeType} { - somethingElse - } - } - } - } - `; - - const token = createBearerToken(secret, { roles: ["admin"], jwtAllowedNamesExample: "somenode" }); - const queryResult = await graphqlQuery(query, token); - expect(queryResult.errors).toBeUndefined(); - expect(queryResult.data).toEqual({ - myInterfaces: [ - { - id: "4", - someField: "bla", - }, - ], - }); - }); }); describe("add limit directive", () => { diff --git a/packages/graphql/tests/integration/experimental/typename-in-auth.int.test.ts b/packages/graphql/tests/integration/experimental/typename-in-auth.int.test.ts new file mode 100644 index 00000000000..025d8c28ee4 --- /dev/null +++ b/packages/graphql/tests/integration/experimental/typename-in-auth.int.test.ts @@ -0,0 +1,448 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { GraphQLSchema } from "graphql"; +import { graphql } from "graphql"; +import type { Driver } from "neo4j-driver"; +import { Neo4jGraphQL } from "../../../src"; +import { cleanNodes } from "../../utils/clean-nodes"; +import { UniqueType } from "../../utils/graphql-types"; +import Neo4j from "../neo4j"; +import { createBearerToken } from "../../utils/create-bearer-token"; + +describe("typename_IN with auth", () => { + let schema: GraphQLSchema; + let neo4j: Neo4j; + let driver: Driver; + let typeDefs: string; + const secret = "secret"; + + const Movie = new UniqueType("Movie"); + const Actor = new UniqueType("Actor"); + const Series = new UniqueType("Series"); + const Cartoon = new UniqueType("Cartoon"); + + async function graphqlQuery(query: string, token: string) { + return graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues({ token }), + }); + } + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + + typeDefs = ` + interface Production { + title: String! + cost: Float! + } + + type ${Movie.name} implements Production { + title: String! + cost: Float! + runtime: Int! + } + + type ${Series.name} implements Production { + title: String! + cost: Float! + episodes: Int! + } + + type ${Cartoon.name} implements Production { + title: String! + cost: Float! + cartoonist: String! + } + + interface ActedIn @relationshipProperties { + screenTime: Int! + } + + type ${Actor.name} { + name: String! + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + `; + + const session = await neo4j.getSession(); + + try { + await session.run(` + CREATE(a:${Actor.name} { name: "Keanu" }) + CREATE(a)-[:ACTED_IN]->(m:${Movie.name} { title: "The Matrix" }) + CREATE(a)-[:ACTED_IN]->(s:${Series.name} { title: "The Matrix animated series" }) + CREATE(a)-[:ACTED_IN]->(c:${Cartoon.name} { title: "Matrix the cartoon" }) + `); + } finally { + await session.close(); + } + }); + + afterAll(async () => { + const session = await neo4j.getSession(); + await cleanNodes(session, [Movie, Series, Cartoon]); + await session.close(); + await driver.close(); + }); + + test("should pass with a correct validate typename predicate", async () => { + const authTypeDefs = + typeDefs + + /* GraphQL */ ` + extend type ${Actor.name} + @authorization( + validate: [ + { + when: [BEFORE] + operations: [READ] + where: { + node: { + actedInConnection_SOME: { node: { AND: [ { title: "The Matrix" }, {typename_IN: [${Movie.name}] }] } } + } + } + } + ] + ) + `; + const neoGraphql = new Neo4jGraphQL({ + typeDefs: authTypeDefs, + driver, + experimental: true, + features: { + authorization: { + key: secret, + }, + }, + }); + schema = await neoGraphql.getSchema(); + const query = ` + { + ${Actor.plural} { + name + } + } + `; + const token = createBearerToken(secret, {}); + + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + [Actor.plural]: expect.toIncludeSameMembers([ + { + name: "Keanu", + }, + ]), + }); + }); + + test("should fail with an incorrect validate typename predicate", async () => { + const authTypeDefs = + typeDefs + + /* GraphQL */ ` + extend type ${Actor.name} + @authorization( + validate: [ + { + when: [BEFORE] + operations: [READ] + where: { + node: { + actedInConnection_SOME: { node: { AND: [ { title: "The Matrix" }, {typename_IN: [${Series.name}] }] } } + } + } + } + ] + ) + `; + const neoGraphql = new Neo4jGraphQL({ + typeDefs: authTypeDefs, + driver, + experimental: true, + features: { + authorization: { + key: secret, + }, + }, + }); + schema = await neoGraphql.getSchema(); + const query = ` + { + ${Actor.plural} { + name + } + } + `; + const token = createBearerToken(secret, {}); + + const queryResult = await graphqlQuery(query, token); + + expect(queryResult.errors?.[0]?.message).toContain("Forbidden"); + }); + + test("should pass with a correct validate typename predicate (field level)", async () => { + const authTypeDefs = + typeDefs + + /* GraphQL */ ` + extend type ${Actor.name} + { + name: String! @authorization( + validate: [ + { + when: [BEFORE] + operations: [READ] + where: { + node: { + actedInConnection_SOME: { node: { AND: [ { title: "The Matrix" }, {typename_IN: [${Movie.name}] }] } } + } + } + } + ] + ) + } + `; + const neoGraphql = new Neo4jGraphQL({ + typeDefs: authTypeDefs, + driver, + experimental: true, + features: { + authorization: { + key: secret, + }, + }, + }); + schema = await neoGraphql.getSchema(); + const query = ` + { + ${Actor.plural} { + name + } + } + `; + const token = createBearerToken(secret, {}); + + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + [Actor.plural]: expect.toIncludeSameMembers([ + { + name: "Keanu", + }, + ]), + }); + }); + + test("should fail with an incorrect validate typename predicate (field level)", async () => { + const authTypeDefs = + typeDefs + + /* GraphQL */ ` + extend type ${Actor.name} + { + name: String! @authorization( + validate: [ + { + when: [BEFORE] + operations: [READ] + where: { + node: { + actedInConnection_SOME: { node: { AND: [ { title: "The Matrix" }, {typename_IN: [${Series.name}] }] } } + } + } + } + ] + ) + } + `; + const neoGraphql = new Neo4jGraphQL({ + typeDefs: authTypeDefs, + driver, + experimental: true, + features: { + authorization: { + key: secret, + }, + }, + }); + schema = await neoGraphql.getSchema(); + const query = ` + { + ${Actor.plural} { + name + } + } + `; + const token = createBearerToken(secret, {}); + + const queryResult = await graphqlQuery(query, token); + + expect(queryResult.errors?.[0]?.message).toContain("Forbidden"); + }); + + test("should pass with an incorrect validate typename predicate (field level), when field is not selected", async () => { + const authTypeDefs = + typeDefs + + /* GraphQL */ ` + extend type ${Actor.name} + { + name: String! @authorization( + validate: [ + { + when: [BEFORE] + operations: [READ] + where: { + node: { + actedInConnection_SOME: { node: { AND: [ { title: "The Matrix" }, {typename_IN: [${Series.name}] }] } } + } + } + } + ] + ) + } + `; + const neoGraphql = new Neo4jGraphQL({ + typeDefs: authTypeDefs, + driver, + experimental: true, + features: { + authorization: { + key: secret, + }, + }, + }); + schema = await neoGraphql.getSchema(); + const query = ` + { + ${Actor.plural} { + actedIn { + title + } + } + } + `; + const token = createBearerToken(secret, {}); + + const queryResult = await graphqlQuery(query, token); + + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + [Actor.plural]: expect.toIncludeSameMembers([ + { + actedIn: expect.arrayContaining([ + { + title: "The Matrix", + }, + ]), + }, + ]), + }); + }); + + test("should return data with a correct filter typename predicate", async () => { + const authTypeDefs = + typeDefs + + /* GraphQL */ ` + extend type ${Actor.name} + @authorization( + filter: [ + { + where: { + node: { + actedInConnection_SOME: { node: { AND: [ { title: "The Matrix" }, {typename_IN: [${Movie.name}] }] } } + } + } + } + ] + ) + `; + const neoGraphql = new Neo4jGraphQL({ + typeDefs: authTypeDefs, + driver, + experimental: true, + features: { + authorization: { + key: secret, + }, + }, + }); + schema = await neoGraphql.getSchema(); + const query = ` + { + ${Actor.plural} { + name + } + } + `; + const token = createBearerToken(secret, {}); + + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + [Actor.plural]: expect.toIncludeSameMembers([ + { + name: "Keanu", + }, + ]), + }); + }); + + test("should not return data with an incorrect filter typename predicate", async () => { + const authTypeDefs = + typeDefs + + /* GraphQL */ ` + extend type ${Actor.name} + @authorization( + filter: [ + { + where: { + node: { + actedInConnection_SOME: { node: { AND: [ { title: "The Matrix" }, {typename_IN: [${Series.name}] }] } } + } + } + } + ] + ) + `; + const neoGraphql = new Neo4jGraphQL({ + typeDefs: authTypeDefs, + driver, + experimental: true, + features: { + authorization: { + key: secret, + }, + }, + }); + schema = await neoGraphql.getSchema(); + const query = ` + { + ${Actor.plural} { + name + } + } + `; + const token = createBearerToken(secret, {}); + + const queryResult = await graphqlQuery(query, token); + + expect(queryResult.data).toEqual({ [Actor.plural]: expect.toBeArrayOfSize(0) }); + }); +}); diff --git a/packages/graphql/tests/integration/experimental/typename-in.int.test.ts b/packages/graphql/tests/integration/experimental/typename-in.int.test.ts new file mode 100644 index 00000000000..85b0aa48174 --- /dev/null +++ b/packages/graphql/tests/integration/experimental/typename-in.int.test.ts @@ -0,0 +1,215 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { GraphQLSchema } from "graphql"; +import { graphql } from "graphql"; +import type { Driver } from "neo4j-driver"; +import { Neo4jGraphQL } from "../../../src"; +import { cleanNodes } from "../../utils/clean-nodes"; +import { UniqueType } from "../../utils/graphql-types"; +import Neo4j from "../neo4j"; + +describe("typename_IN", () => { + let schema: GraphQLSchema; + let neo4j: Neo4j; + let driver: Driver; + let typeDefs: string; + + const Movie = new UniqueType("Movie"); + const Actor = new UniqueType("Actor"); + const Series = new UniqueType("Series"); + const Cartoon = new UniqueType("Cartoon"); + + async function graphqlQuery(query: string, token?: string) { + return graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues({ token }), + }); + } + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + + typeDefs = ` + interface Production { + title: String! + cost: Float! + } + + type ${Movie.name} implements Production { + title: String! + cost: Float! + runtime: Int! + } + + type ${Series.name} implements Production { + title: String! + cost: Float! + episodes: Int! + } + + type ${Cartoon.name} implements Production { + title: String! + cost: Float! + cartoonist: String! + } + + interface ActedIn @relationshipProperties { + screenTime: Int! + } + + type ${Actor.name} { + name: String! + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + `; + + const session = await neo4j.getSession(); + + try { + await session.run(` + CREATE(a:${Actor.name} { name: "Keanu" }) + CREATE(a)-[:ACTED_IN]->(m:${Movie.name} { title: "The Matrix" }) + CREATE(a)-[:ACTED_IN]->(s:${Series.name} { title: "The Matrix animated series" }) + CREATE(a)-[:ACTED_IN]->(c:${Cartoon.name} { title: "Matrix the cartoon" }) + `); + } finally { + await session.close(); + } + + const neoGraphql = new Neo4jGraphQL({ + typeDefs, + driver, + experimental: true, + }); + schema = await neoGraphql.getSchema(); + }); + + afterAll(async () => { + const session = await neo4j.getSession(); + await cleanNodes(session, [Movie, Series, Cartoon]); + await session.close(); + await driver.close(); + }); + + test("top-level", async () => { + const query = ` + { + productions(where: { OR: [{ AND: [{ title: "The Matrix" }, { typename_IN: [${Movie.name}] }] }, { typename_IN: [${Series.name}] }]}) { + __typename + title + } + } + `; + + const queryResult = await graphqlQuery(query); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + productions: expect.toIncludeSameMembers([ + { + __typename: Movie.name, + title: "The Matrix", + }, + { + __typename: Series.name, + title: "The Matrix animated series", + }, + ]), + }); + }); + + test("nested", async () => { + const query = ` + { + ${Actor.plural} { + actedIn(where: { OR: [ + { AND: [{ title: "The Matrix" }, { typename_IN: [${Movie.name}] }] } + { typename_IN: [${Series.name}] } + ] }) { + __typename + title + } + } + } + `; + + const queryResult = await graphqlQuery(query); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + [Actor.plural]: [ + { + actedIn: expect.toIncludeSameMembers([ + { + __typename: Movie.name, + title: "The Matrix", + }, + { + __typename: Series.name, + title: "The Matrix animated series", + }, + ]), + }, + ], + }); + }); + + test("aggregation", async () => { + const query = ` + { + productionsAggregate(where: { OR: [ { typename_IN: [${Movie.name}, ${Series.name}] } { typename_IN: [${Cartoon.name}] } ] }) { + count + } + } + `; + + const queryResult = await graphqlQuery(query); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + productionsAggregate: { + count: 3, + }, + }); + }); + + test("nested aggregation", async () => { + const query = ` + { + ${Actor.plural} { + actedInAggregate(where: { NOT: { typename_IN: [${Movie.name}, ${Series.name}] } }) { + count + } + } + } + `; + + const queryResult = await graphqlQuery(query); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + [Actor.plural]: expect.arrayContaining([ + { + actedInAggregate: { + count: 1, + }, + }, + ]), + }); + }); +}); diff --git a/packages/graphql/tests/integration/experimental/union-relationship-filtering.int.test.ts b/packages/graphql/tests/integration/experimental/union-relationship-filtering.int.test.ts new file mode 100644 index 00000000000..eeec2d01e51 --- /dev/null +++ b/packages/graphql/tests/integration/experimental/union-relationship-filtering.int.test.ts @@ -0,0 +1,229 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { GraphQLSchema } from "graphql"; +import { graphql } from "graphql"; +import type { Driver } from "neo4j-driver"; +import { Neo4jGraphQL } from "../../../src"; +import { cleanNodes } from "../../utils/clean-nodes"; +import { createBearerToken } from "../../utils/create-bearer-token"; +import { UniqueType } from "../../utils/graphql-types"; +import Neo4j from "../neo4j"; + +describe("Union filtering", () => { + const secret = "the-secret"; + + let schema: GraphQLSchema; + let neo4j: Neo4j; + let driver: Driver; + let typeDefs: string; + + const Movie = new UniqueType("Movie"); + const Series = new UniqueType("Series"); + const Actor = new UniqueType("Actor"); + + async function graphqlQuery(query: string, token: string) { + return graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues({ token }), + }); + } + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + + typeDefs = /* GraphQL */ ` + union Production = ${Movie} | ${Series} + + type ${Movie} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type ${Series} { + title: String! + actors: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type ${Actor} { + name: String! + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + interface ActedIn @relationshipProperties { + screenTime: Int! + } + `; + + const session = await neo4j.getSession(); + + try { + await session.run(` + CREATE(m1:${Movie} { title: "The Office" }) + CREATE(m2:${Movie} { title: "The Office 2" }) + CREATE(m3:${Movie} { title: "NOT The Office 2" }) + CREATE(s1:${Series} { title: "The Office 2" }) + CREATE(s2:${Series} { title: "NOT The Office" }) + CREATE(a1:${Actor} {name: "Keanu"}) + CREATE(a2:${Actor} {name: "Michael"}) + CREATE(a3:${Actor} {name: "John"}) + MERGE(a1)-[:ACTED_IN]->(m1) + MERGE(a1)-[:ACTED_IN]->(s2) + MERGE(a2)-[:ACTED_IN]->(m2) + MERGE(a2)-[:ACTED_IN]->(m3) + MERGE(a2)-[:ACTED_IN]->(s1) + MERGE(a3)-[:ACTED_IN]->(s1) + MERGE(a3)-[:ACTED_IN]->(s2) + `); + } finally { + await session.close(); + } + + const neoGraphql = new Neo4jGraphQL({ + typeDefs, + driver, + features: { + authorization: { + key: secret, + }, + }, + experimental: true, + }); + schema = await neoGraphql.getSchema(); + }); + + afterAll(async () => { + const session = await neo4j.getSession(); + await cleanNodes(session, [Movie, Series, Actor]); + await session.close(); + await driver.close(); + }); + + test("allow for filtering on top-level union relationships", async () => { + const query = /* GraphQL */ ` + query { + productions(where: { ${Movie}: { title: "The Office" }, ${Series}: { title: "The Office 2" } }) { + ... on ${Movie} { + title + } + ... on ${Series} { + title + } + } + } + `; + + const token = createBearerToken(secret, {}); + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + productions: expect.toIncludeSameMembers([ + { + title: "The Office", + }, + { + title: "The Office 2", + }, + ]), + }); + }); + + test("allow for filtering on nested-level relationship unions", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural}(where: { + actedIn_SOME: { ${Movie}: { title_CONTAINS: "Office" }} + }) { + name + } + } + `; + + const token = createBearerToken(secret, {}); + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + [Actor.plural]: expect.toIncludeSameMembers([ + { + name: "Keanu", + }, + { + name: "Michael", + }, + ]), + }); + }); + + test("allow updating an actor name based on a union relationship filter", async () => { + const query = /* GraphQL */ ` + mutation updateName($name: String!) { + ${Actor.operations.update}( + where: { actedIn_SOME: { ${Movie}: { title: "The Office" } }}, + update: { name: $name } + ) { + ${Actor.plural} { + name + actedIn { + __typename + ... on ${Movie} { + title + } + ... on ${Series} { + title + } + } + } + } + } + `; + + const token = createBearerToken(secret, {}); + const queryResult = await graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues({ token }), + variableValues: { + name: "Michael Scott", + }, + }); + + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + [Actor.operations.update]: { + [Actor.plural]: [ + { + name: "Michael Scott", + actedIn: [ + { + __typename: Movie.name, + title: "The Office", + }, + { + __typename: Series.name, + title: "NOT The Office", + }, + ], + }, + ], + }, + }); + }); +}); diff --git a/packages/graphql/tests/integration/issues/1683.int.test.ts b/packages/graphql/tests/integration/issues/1683.int.test.ts index 55eb64f0fea..0d6bcad8c81 100644 --- a/packages/graphql/tests/integration/issues/1683.int.test.ts +++ b/packages/graphql/tests/integration/issues/1683.int.test.ts @@ -20,9 +20,9 @@ import type { GraphQLSchema } from "graphql"; import { graphql } from "graphql"; import type { Driver } from "neo4j-driver"; -import Neo4j from "../neo4j"; import { Neo4jGraphQL } from "../../../src"; import { UniqueType } from "../../utils/graphql-types"; +import Neo4j from "../neo4j"; describe("https://github.com/neo4j/graphql/issues/1683", () => { const systemType = new UniqueType("System"); diff --git a/packages/graphql/tests/integration/issues/1686.int.test.ts b/packages/graphql/tests/integration/issues/1686.int.test.ts index 37295185a7d..7da25397708 100644 --- a/packages/graphql/tests/integration/issues/1686.int.test.ts +++ b/packages/graphql/tests/integration/issues/1686.int.test.ts @@ -20,9 +20,9 @@ import type { GraphQLSchema } from "graphql"; import { graphql } from "graphql"; import type { Driver } from "neo4j-driver"; -import Neo4j from "../neo4j"; import { Neo4jGraphQL } from "../../../src"; import { UniqueType } from "../../utils/graphql-types"; +import Neo4j from "../neo4j"; describe("https://github.com/neo4j/graphql/issues/1686", () => { const productionType = new UniqueType("Production"); diff --git a/packages/graphql/tests/integration/issues/4405.int.test.ts b/packages/graphql/tests/integration/issues/4405.int.test.ts new file mode 100644 index 00000000000..5b7af981a94 --- /dev/null +++ b/packages/graphql/tests/integration/issues/4405.int.test.ts @@ -0,0 +1,136 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Driver } from "neo4j-driver"; +import Neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../src/classes"; +import { graphql } from "graphql"; +import { UniqueType } from "../../utils/graphql-types"; +import { cleanNodes } from "../../utils/clean-nodes"; + +describe("https://github.com/neo4j/graphql/issues/4405", () => { + let driver: Driver; + let neo4j: Neo4j; + let neo4jGraphql: Neo4jGraphQL; + const Movie = new UniqueType("Movie"); + const Actor = new UniqueType("Actor"); + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + const typeDefs = /* GraphQL */ ` + type ${Movie.name} { + title: String + } + + type ${Actor.name} + @authorization( + validate: [ + { + when: [BEFORE] + operations: [READ] + where: { node: { actedInConnection_SOME: { node: { title_IN: ["Matrix", "John Wick"] } } } } + } + ] + ) { + name: String! + actedIn: [${Movie.name}!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + neo4jGraphql = new Neo4jGraphQL({ + typeDefs, + driver, + features: { + authorization: { + key: "secret", + }, + }, + }); + + const session = await neo4j.getSession(); + try { + await session.run( + ` + CREATE (m:${Movie.name} {title: "Matrix" })<-[:ACTED_IN]-(a:${Actor.name} { name: "Keanu"}) + CREATE (a)-[:ACTED_IN]->(:${Movie.name} {title: "John Wick" }) + CREATE (m2:${Movie.name} {title: "Hunger games" })<-[:ACTED_IN]-(a2:${Actor.name} { name: "Laurence"}) + + `, + {} + ); + } finally { + await session.close(); + } + }); + + afterAll(async () => { + const session = await neo4j.getSession(); + try { + await cleanNodes(session, [Movie.name, Actor.name]); + } finally { + await session.close(); + } + await driver.close(); + }); + + test("should not raise if title is the filter array value", async () => { + const schema = await neo4jGraphql.getSchema(); + + const query = /* GraphQL */ ` + query Actors { + ${Actor.plural}(where: { name: "Keanu"}) { + name + } + } + `; + + const response = await graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues({ jwt: { uid: "user-1" } }), + }); + expect(response.errors).toBeFalsy(); + expect(response.data?.[Actor.plural]).toStrictEqual( + expect.arrayContaining([ + { + name: "Keanu", + }, + ]) + ); + }); + + test("should raise if title is not in the filter array value", async () => { + const schema = await neo4jGraphql.getSchema(); + + const query = /* GraphQL */ ` + query Actors { + ${Actor.plural}(where: { name: "Laurence"}) { + name + } + } + `; + + const response = await graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues({ jwt: { uid: "user-1" } }), + }); + expect(response.errors?.[0]?.message).toContain("Forbidden"); + }); +}); diff --git a/packages/graphql/tests/integration/issues/4429.int.test.ts b/packages/graphql/tests/integration/issues/4429.int.test.ts new file mode 100644 index 00000000000..0026ec8040f --- /dev/null +++ b/packages/graphql/tests/integration/issues/4429.int.test.ts @@ -0,0 +1,227 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Driver } from "neo4j-driver"; +import Neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../src/classes"; +import { graphql } from "graphql"; +import { UniqueType } from "../../utils/graphql-types"; +import gql from "graphql-tag"; + +describe("https://github.com/neo4j/graphql/issues/4429", () => { + let driver: Driver; + let neo4j: Neo4j; + + const User = new UniqueType("User"); + const Tenant = new UniqueType("Tenant"); + const Settings = new UniqueType("Settings"); + const OpeningDay = new UniqueType("OpeningDay"); + const OpeningHoursInterval = new UniqueType("OpeningHoursInterval"); + + const typeDefs = gql` + type JWT @jwt { + id: String + roles: [String] + } + type ${User.name} @authorization(validate: [{ where: { node: { userId: "$jwt.id" } }, operations: [READ] }]) { + userId: String! @unique + adminAccess: [${Tenant.name}!]! @relationship(type: "ADMIN_IN", direction: OUT) + } + + type ${Tenant.name} @authorization(validate: [{ where: { node: { admins: { userId: "$jwt.id" } } } }]) { + id: ID! @id + settings: ${Settings.name}! @relationship(type: "VEHICLECARD_OWNER", direction: IN) + admins: [${User.name}!]! @relationship(type: "ADMIN_IN", direction: IN) + } + + type ${Settings.name} @authorization(validate: [{ where: { node: { tenant: { admins: { userId: "$jwt.id" } } } } }]) { + id: ID! @id + openingDays: [${OpeningDay.name}!]! @relationship(type: "VALID_GARAGES", direction: OUT) + tenant: ${Tenant.name}! @relationship(type: "VEHICLECARD_OWNER", direction: OUT) + } + + type ${OpeningDay.name} + @authorization( + validate: [{ where: { node: { settings: { tenant: { admins: { userId: "$jwt.id" } } } } } }] + ) { + id: ID! @id + settings: ${Settings.name} @relationship(type: "VALID_GARAGES", direction: IN) + open: [${OpeningHoursInterval.name}!]! @relationship(type: "HAS_OPEN_INTERVALS", direction: OUT) + } + + type ${OpeningHoursInterval.name} + @authorization( + validate: [ + { where: { node: { openingDay: { settings: { tenant: { admins: { userId: "$jwt.id" } } } } } } } + ] + ) { + name: String + openingDay: ${OpeningDay.name}! @relationship(type: "HAS_OPEN_INTERVALS", direction: IN) + createdAt: DateTime! @timestamp(operations: [CREATE]) + updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE]) + updatedBy: String + @populatedBy( + callback: "getUserIDFromContext" + operations: [CREATE, UPDATE] + ) + } + `; + + const ADD_TENANT = ` + mutation addTenant($input: [${Tenant.name}CreateInput!]!) { + ${Tenant.operations.create}(input: $input) { + ${Tenant.plural} { + id + admins { + userId + } + settings { + openingDays { + open { + name + } + } + } + } + } + } + `; + + let tenantVariables: Record; + let myUserId: string; + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + }); + + beforeEach(() => { + myUserId = Math.random().toString(36).slice(2, 7); + tenantVariables = { + input: { + admins: { + create: { + node: { + userId: myUserId, + }, + }, + }, + settings: { + create: { + node: { + openingDays: { + create: [ + { + node: { + open: { + create: [ + { + node: { + name: "hi", + }, + }, + ], + }, + }, + }, + { + node: { + open: { + create: [ + { + node: { + name: "hi", + }, + }, + { + node: { + name: "hi", + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + }, + }, + }; + }); + + afterEach(async () => { + const session = driver.session(); + await session.run(` match (n) detach delete n`); + await session.close(); + }); + + afterAll(async () => { + await driver.close(); + }); + test("create tenant with nested openingDays and openHoursInterval - subscriptions disabled", async () => { + const neo4jGraphql = new Neo4jGraphQL({ + typeDefs, + driver, + features: { + populatedBy: { + callbacks: { + getUserIDFromContext: () => "hi", + }, + }, + }, + }); + const schema = await neo4jGraphql.getSchema(); + + const addTenantResponse = await graphql({ + schema, + source: ADD_TENANT, + variableValues: tenantVariables, + contextValue: neo4j.getContextValues({ jwt: { id: myUserId } }), + }); + + expect(addTenantResponse.errors).toBeUndefined(); + }); + + test("create tenant with nested openingDays and openHoursInterval - subscriptions enabled", async () => { + const neo4jGraphql = new Neo4jGraphQL({ + typeDefs, + driver, + features: { + subscriptions: true, + populatedBy: { + callbacks: { + getUserIDFromContext: () => "hi", + }, + }, + }, + }); + const schema = await neo4jGraphql.getSchema(); + + const addTenantResponse = await graphql({ + schema, + source: ADD_TENANT, + variableValues: tenantVariables, + contextValue: neo4j.getContextValues({ jwt: { id: myUserId } }), + }); + + expect(addTenantResponse.errors).toBeUndefined(); + }); +}); diff --git a/packages/graphql/tests/integration/issues/4450.int.test.ts b/packages/graphql/tests/integration/issues/4450.int.test.ts new file mode 100644 index 00000000000..349f0b73faa --- /dev/null +++ b/packages/graphql/tests/integration/issues/4450.int.test.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { graphql } from "graphql"; +import type { Driver } from "neo4j-driver"; +import { Neo4jGraphQL } from "../../../src/classes"; +import { cleanNodes } from "../../utils/clean-nodes"; +import { UniqueType } from "../../utils/graphql-types"; +import Neo4j from "../neo4j"; + +describe("https://github.com/neo4j/graphql/issues/4450", () => { + let driver: Driver; + let neo4j: Neo4j; + let neo4jGraphql: Neo4jGraphQL; + + const Actor = new UniqueType("Actor"); + const Scene = new UniqueType("Scene"); + const Location = new UniqueType("Location"); + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + const typeDefs = /* GraphQL */ ` + type ${Actor} { + name: String + scene: [${Scene}!]! @relationship(type: "IN_SCENE", properties: "ActorScene", direction: OUT) + } + + type ${Scene} { + number: Int + actors: [${Actor}!]! @relationship(type: "IN_SCENE", properties: "ActorScene", direction: IN) + location: ${Location}! @relationship(type: "AT_LOCATION", direction: OUT) + } + + type ${Location} { + city: String + scenes: [${Scene}!]! @relationship(type: "AT_LOCATION", direction: IN) + } + + interface ActorScene @relationshipProperties { + cut: Boolean + } + `; + + neo4jGraphql = new Neo4jGraphQL({ + typeDefs, + driver, + }); + + const session = await neo4j.getSession(); + try { + await session.run( + ` + CREATE (:${Actor} {name: "actor-1"})-[:IN_SCENE {cut: true}]->(:${Scene} {number: 1})-[:AT_LOCATION]->(:${Location} {city: "test"}) + `, + {} + ); + } finally { + await session.close(); + } + }); + + afterAll(async () => { + const session = await neo4j.getSession(); + try { + await cleanNodes(session, [Actor, Scene, Location]); + } finally { + await session.close(); + } + await driver.close(); + }); + + test("filtering through a connection to a many-to-1 relationship should work", async () => { + const schema = await neo4jGraphql.getSchema(); + + const query = /* GraphQL */ ` + query { + ${Actor.plural}(where: { sceneConnection_SOME: { edge: { cut: true }, node: { location: { city: "test" } } } }) { + name + } + } + `; + + const response = await graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues(), + }); + + expect(response.errors).toBeFalsy(); + expect(response.data).toEqual({ + [Actor.plural]: [ + { + name: "actor-1", + }, + ], + }); + }); +}); diff --git a/packages/graphql/tests/integration/issues/4477.int.test.ts b/packages/graphql/tests/integration/issues/4477.int.test.ts new file mode 100644 index 00000000000..a7d1bb1fa7c --- /dev/null +++ b/packages/graphql/tests/integration/issues/4477.int.test.ts @@ -0,0 +1,150 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { graphql } from "graphql"; +import type { Driver } from "neo4j-driver"; +import { Neo4jGraphQL } from "../../../src/classes"; +import { cleanNodes } from "../../utils/clean-nodes"; +import { UniqueType } from "../../utils/graphql-types"; +import Neo4j from "../neo4j"; + +describe("https://github.com/neo4j/graphql/issues/4477", () => { + let driver: Driver; + let neo4j: Neo4j; + let neo4jGraphql: Neo4jGraphQL; + + const Brand = new UniqueType("Brand"); + const Service = new UniqueType("Service"); + const Collection = new UniqueType("Collection"); + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + const typeDefs = /* GraphQL */ ` + type ${Brand} { + services: [${Service}!]! @relationship(type: "HAS_SERVICE", direction: OUT) + name: String! + } + + type ${Collection} { + services: [${Service}!]! @relationship(type: "HAS_SERVICE", direction: OUT) + } + + type ${Service} { + collection: ${Collection} @relationship(type: "HAS_SERVICE", direction: IN) + } + `; + + neo4jGraphql = new Neo4jGraphQL({ + typeDefs, + driver, + }); + + const session = await neo4j.getSession(); + try { + await session.run( + ` + CREATE + (brand1:${Brand} {name: 'brand1'}), + (brand2:${Brand} {name: 'brand2'}), + (collection1:${Collection}), + (collection2:${Collection}), + (service1:${Service}), + (service2:${Service}), + (service3:${Service}), + (service4:${Service}), + (brand1)-[:HAS_SERVICE]->(service1), + (brand1)-[:HAS_SERVICE]->(service2), + (brand2)-[:HAS_SERVICE]->(service3), + (brand2)-[:HAS_SERVICE]->(service4), + (collection1)-[:HAS_SERVICE]->(service1), + (collection1)-[:HAS_SERVICE]->(service2), + (collection2)-[:HAS_SERVICE]->(service3) + `, + {} + ); + } finally { + await session.close(); + } + }); + + afterAll(async () => { + const session = await neo4j.getSession(); + try { + await cleanNodes(session, [Brand, Service, Collection]); + } finally { + await session.close(); + } + await driver.close(); + }); + + test("filtering by count on an aggregate should work", async () => { + const schema = await neo4jGraphql.getSchema(); + + const query = /* GraphQL */ ` + query { + ${Brand.plural} { + name + services(where: { collectionAggregate: { count: 1 } }) { + collectionAggregate { + count + } + } + } + } + `; + + const response = await graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues(), + }); + + expect(response.errors).toBeFalsy(); + expect(response.data).toEqual({ + [Brand.plural]: [ + { + name: "brand1", + services: [ + { + collectionAggregate: { + count: 1, + }, + }, + { + collectionAggregate: { + count: 1, + }, + }, + ], + }, + { + name: "brand2", + services: [ + { + collectionAggregate: { + count: 1, + }, + }, + ], + }, + ], + }); + }); +}); diff --git a/packages/graphql/tests/integration/relationship-properties/read.int.test.ts b/packages/graphql/tests/integration/relationship-properties/read.int.test.ts index ccb01adaf1e..c5e72462e1f 100644 --- a/packages/graphql/tests/integration/relationship-properties/read.int.test.ts +++ b/packages/graphql/tests/integration/relationship-properties/read.int.test.ts @@ -17,17 +17,17 @@ * limitations under the License. */ -import { offsetToCursor } from "graphql-relay"; -import type { Driver } from "neo4j-driver"; import type { DocumentNode } from "graphql"; import { graphql } from "graphql"; +import { offsetToCursor } from "graphql-relay"; import { gql } from "graphql-tag"; +import type { Driver } from "neo4j-driver"; import { generate } from "randomstring"; -import Neo4j from "../neo4j"; import { Neo4jGraphQL } from "../../../src/classes"; +import { cleanNodes } from "../../utils/clean-nodes"; import { UniqueType } from "../../utils/graphql-types"; import { runCypher } from "../../utils/run-cypher"; -import { cleanNodes } from "../../utils/clean-nodes"; +import Neo4j from "../neo4j"; describe("Relationship properties - read", () => { let driver: Driver; diff --git a/packages/graphql/tests/integration/sort.int.test.ts b/packages/graphql/tests/integration/sort.int.test.ts index 03bf7aab5c0..c3921552b43 100644 --- a/packages/graphql/tests/integration/sort.int.test.ts +++ b/packages/graphql/tests/integration/sort.int.test.ts @@ -17,14 +17,14 @@ * limitations under the License. */ -import type { Driver, Session } from "neo4j-driver"; import type { GraphQLSchema } from "graphql"; import { graphql } from "graphql"; -import { generate } from "randomstring"; import { gql } from "graphql-tag"; -import Neo4j from "./neo4j"; +import type { Driver, Session } from "neo4j-driver"; +import { generate } from "randomstring"; import { Neo4jGraphQL } from "../../src/classes"; import { UniqueType } from "../utils/graphql-types"; +import Neo4j from "./neo4j"; const testLabel = generate({ charset: "alphabetic" }); diff --git a/packages/graphql/tests/schema/directives/plural.test.ts b/packages/graphql/tests/schema/directives/plural.test.ts index e907926d717..4880f400d87 100644 --- a/packages/graphql/tests/schema/directives/plural.test.ts +++ b/packages/graphql/tests/schema/directives/plural.test.ts @@ -18,8 +18,8 @@ */ import { printSchemaWithDirectives } from "@graphql-tools/utils"; -import { lexicographicSortSchema } from "graphql/utilities"; import { gql } from "graphql-tag"; +import { lexicographicSortSchema } from "graphql/utilities"; import { Neo4jGraphQL } from "../../../src"; describe("Plural option", () => { @@ -1138,314 +1138,4 @@ describe("Plural option", () => { }" `); }); - - test("Plural on interface and union", async () => { - const typeDefs = gql` - interface Animal @plural(value: "animales") { - name: String - } - - type Dog implements Animal { - name: String - breed: String - } - - type Cat { - queenOf: String - } - - union Pet @plural(value: "petties") = Dog | Cat - `; - const neoSchema = new Neo4jGraphQL({ typeDefs, experimental: true }); - const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); - - expect(printedSchema).toMatchInlineSnapshot(` - "schema { - query: Query - mutation: Mutation - } - - interface Animal { - name: String - } - - type AnimalAggregateSelection { - count: Int! - name: StringAggregateSelectionNullable! - } - - input AnimalImplementationsWhere { - Dog: DogWhere - } - - input AnimalOptions { - limit: Int - offset: Int - \\"\\"\\" - Specify one or more AnimalSort objects to sort Animales by. The sorts will be applied in the order in which they are arranged in the array. - \\"\\"\\" - sort: [AnimalSort] - } - - \\"\\"\\" - Fields to sort Animales by. The order in which sorts are applied is not guaranteed when specifying many fields in one AnimalSort object. - \\"\\"\\" - input AnimalSort { - name: SortDirection - } - - input AnimalWhere { - _on: AnimalImplementationsWhere - name: String - name_CONTAINS: String - name_ENDS_WITH: String - name_IN: [String] - name_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - name_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - name_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - name_STARTS_WITH: String - } - - type Cat { - queenOf: String - } - - type CatAggregateSelection { - count: Int! - queenOf: StringAggregateSelectionNullable! - } - - input CatCreateInput { - queenOf: String - } - - type CatEdge { - cursor: String! - node: Cat! - } - - input CatOptions { - limit: Int - offset: Int - \\"\\"\\" - Specify one or more CatSort objects to sort Cats by. The sorts will be applied in the order in which they are arranged in the array. - \\"\\"\\" - sort: [CatSort!] - } - - \\"\\"\\" - Fields to sort Cats by. The order in which sorts are applied is not guaranteed when specifying many fields in one CatSort object. - \\"\\"\\" - input CatSort { - queenOf: SortDirection - } - - input CatUpdateInput { - queenOf: String - } - - input CatWhere { - AND: [CatWhere!] - NOT: CatWhere - OR: [CatWhere!] - queenOf: String - queenOf_CONTAINS: String - queenOf_ENDS_WITH: String - queenOf_IN: [String] - queenOf_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - queenOf_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - queenOf_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - queenOf_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - queenOf_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - queenOf_STARTS_WITH: String - } - - type CatsConnection { - edges: [CatEdge!]! - pageInfo: PageInfo! - totalCount: Int! - } - - type CreateCatsMutationResponse { - cats: [Cat!]! - info: CreateInfo! - } - - type CreateDogsMutationResponse { - dogs: [Dog!]! - info: CreateInfo! - } - - \\"\\"\\" - Information about the number of nodes and relationships created during a create mutation - \\"\\"\\" - type CreateInfo { - bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") - nodesCreated: Int! - relationshipsCreated: Int! - } - - \\"\\"\\" - Information about the number of nodes and relationships deleted during a delete mutation - \\"\\"\\" - type DeleteInfo { - bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") - nodesDeleted: Int! - relationshipsDeleted: Int! - } - - type Dog implements Animal { - breed: String - name: String - } - - type DogAggregateSelection { - breed: StringAggregateSelectionNullable! - count: Int! - name: StringAggregateSelectionNullable! - } - - input DogCreateInput { - breed: String - name: String - } - - type DogEdge { - cursor: String! - node: Dog! - } - - input DogOptions { - limit: Int - offset: Int - \\"\\"\\" - Specify one or more DogSort objects to sort Dogs by. The sorts will be applied in the order in which they are arranged in the array. - \\"\\"\\" - sort: [DogSort!] - } - - \\"\\"\\" - Fields to sort Dogs by. The order in which sorts are applied is not guaranteed when specifying many fields in one DogSort object. - \\"\\"\\" - input DogSort { - breed: SortDirection - name: SortDirection - } - - input DogUpdateInput { - breed: String - name: String - } - - input DogWhere { - AND: [DogWhere!] - NOT: DogWhere - OR: [DogWhere!] - breed: String - breed_CONTAINS: String - breed_ENDS_WITH: String - breed_IN: [String] - breed_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - breed_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - breed_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - breed_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - breed_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - breed_STARTS_WITH: String - name: String - name_CONTAINS: String - name_ENDS_WITH: String - name_IN: [String] - name_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - name_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - name_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - name_STARTS_WITH: String - } - - type DogsConnection { - edges: [DogEdge!]! - pageInfo: PageInfo! - totalCount: Int! - } - - type Mutation { - createCats(input: [CatCreateInput!]!): CreateCatsMutationResponse! - createDogs(input: [DogCreateInput!]!): CreateDogsMutationResponse! - deleteCats(where: CatWhere): DeleteInfo! - deleteDogs(where: DogWhere): DeleteInfo! - updateCats(update: CatUpdateInput, where: CatWhere): UpdateCatsMutationResponse! - updateDogs(update: DogUpdateInput, where: DogWhere): UpdateDogsMutationResponse! - } - - \\"\\"\\"Pagination information (Relay)\\"\\"\\" - type PageInfo { - endCursor: String - hasNextPage: Boolean! - hasPreviousPage: Boolean! - startCursor: String - } - - union Pet = Cat | Dog - - input PetWhere { - Cat: CatWhere - Dog: DogWhere - } - - type Query { - animales(options: AnimalOptions, where: AnimalWhere): [Animal!]! - animalesAggregate(where: AnimalWhere): AnimalAggregateSelection! - cats(options: CatOptions, where: CatWhere): [Cat!]! - catsAggregate(where: CatWhere): CatAggregateSelection! - catsConnection(after: String, first: Int, sort: [CatSort], where: CatWhere): CatsConnection! - dogs(options: DogOptions, where: DogWhere): [Dog!]! - dogsAggregate(where: DogWhere): DogAggregateSelection! - dogsConnection(after: String, first: Int, sort: [DogSort], where: DogWhere): DogsConnection! - petties(options: QueryOptions, where: PetWhere): [Pet!]! - } - - \\"\\"\\"Input type for options that can be specified on a query operation.\\"\\"\\" - input QueryOptions { - limit: Int - offset: Int - } - - \\"\\"\\"An enum for sorting in either ascending or descending order.\\"\\"\\" - enum SortDirection { - \\"\\"\\"Sort by field values in ascending order.\\"\\"\\" - ASC - \\"\\"\\"Sort by field values in descending order.\\"\\"\\" - DESC - } - - type StringAggregateSelectionNullable { - longest: String - shortest: String - } - - type UpdateCatsMutationResponse { - cats: [Cat!]! - info: UpdateInfo! - } - - type UpdateDogsMutationResponse { - dogs: [Dog!]! - info: UpdateInfo! - } - - \\"\\"\\" - Information about the number of nodes and relationships created and deleted during an update mutation - \\"\\"\\" - type UpdateInfo { - bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") - nodesCreated: Int! - nodesDeleted: Int! - relationshipsCreated: Int! - relationshipsDeleted: Int! - }" - `); - }); }); diff --git a/packages/graphql/tests/schema/directives/timestamps.test.ts b/packages/graphql/tests/schema/directives/timestamps.test.ts index 496f7636170..9667ec5f6f7 100644 --- a/packages/graphql/tests/schema/directives/timestamps.test.ts +++ b/packages/graphql/tests/schema/directives/timestamps.test.ts @@ -91,6 +91,7 @@ describe("Timestamps", () => { input MovieCreateInput { id: ID + updatedAt: DateTime! } type MovieEdge { @@ -117,6 +118,7 @@ describe("Timestamps", () => { } input MovieUpdateInput { + createdAt: DateTime id: ID } diff --git a/packages/graphql/tests/schema/experimental-schema/comments.test.ts b/packages/graphql/tests/schema/experimental-schema/comments.test.ts index 38431941397..ab1b5351cfa 100644 --- a/packages/graphql/tests/schema/experimental-schema/comments.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/comments.test.ts @@ -1098,16 +1098,16 @@ describe("Comments", () => { Series: SeriesCreateInput } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -1130,7 +1130,9 @@ describe("Comments", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] title: String title_CONTAINS: String title_ENDS_WITH: String @@ -1141,6 +1143,7 @@ describe("Comments", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -1603,6 +1606,7 @@ describe("Comments", () => { id_NOT_IN: [ID] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_STARTS_WITH: ID + search: SearchWhere @deprecated(reason: \\"Use \`search_SOME\` instead.\\") searchConnection: MovieSearchConnectionWhere @deprecated(reason: \\"Use \`searchConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieSearchConnections match this filter @@ -1621,6 +1625,15 @@ describe("Comments", () => { Return Movies where some of the related MovieSearchConnections match this filter \\"\\"\\" searchConnection_SOME: MovieSearchConnectionWhere + \\"\\"\\"Return Movies where all of the related Searches match this filter\\"\\"\\" + search_ALL: SearchWhere + \\"\\"\\"Return Movies where none of the related Searches match this filter\\"\\"\\" + search_NONE: SearchWhere + search_NOT: SearchWhere @deprecated(reason: \\"Use \`search_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related Searches match this filter\\"\\"\\" + search_SINGLE: SearchWhere + \\"\\"\\"Return Movies where some of the related Searches match this filter\\"\\"\\" + search_SOME: SearchWhere } type MoviesConnection { diff --git a/packages/graphql/tests/schema/experimental-schema/directive-preserve.test.ts b/packages/graphql/tests/schema/experimental-schema/directive-preserve.test.ts index 1ea0941410c..3944795f8f5 100644 --- a/packages/graphql/tests/schema/experimental-schema/directive-preserve.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/directive-preserve.test.ts @@ -1521,6 +1521,11 @@ describe("Directive-preserve", () => { _on: ProductionImplementationsDisconnectInput } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsConnectInput { Movie: [MovieConnectInput!] Series: [SeriesConnectInput!] @@ -1541,11 +1546,6 @@ describe("Directive-preserve", () => { Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -1568,7 +1568,9 @@ describe("Directive-preserve", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] title: String title_CONTAINS: String title_ENDS_WITH: String @@ -1579,6 +1581,7 @@ describe("Directive-preserve", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -2519,6 +2522,11 @@ describe("Directive-preserve", () => { _on: ProductionImplementationsDisconnectInput } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsConnectInput { Movie: [MovieConnectInput!] Series: [SeriesConnectInput!] @@ -2539,11 +2547,6 @@ describe("Directive-preserve", () => { Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -2566,7 +2569,9 @@ describe("Directive-preserve", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] title: String title_CONTAINS: String title_ENDS_WITH: String @@ -2577,6 +2582,7 @@ describe("Directive-preserve", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -3511,6 +3517,7 @@ describe("Directive-preserve", () => { AND: [UserWhere!] NOT: UserWhere OR: [UserWhere!] + content: ContentWhere @deprecated(reason: \\"Use \`content_SOME\` instead.\\") contentConnection: UserContentConnectionWhere @deprecated(reason: \\"Use \`contentConnection_SOME\` instead.\\") \\"\\"\\" Return Users where all of the related UserContentConnections match this filter @@ -3529,6 +3536,15 @@ describe("Directive-preserve", () => { Return Users where some of the related UserContentConnections match this filter \\"\\"\\" contentConnection_SOME: UserContentConnectionWhere @deprecated(reason: \\"Do not use user.content\\") + \\"\\"\\"Return Users where all of the related Contents match this filter\\"\\"\\" + content_ALL: ContentWhere + \\"\\"\\"Return Users where none of the related Contents match this filter\\"\\"\\" + content_NONE: ContentWhere + content_NOT: ContentWhere @deprecated(reason: \\"Use \`content_NONE\` instead.\\") + \\"\\"\\"Return Users where one of the related Contents match this filter\\"\\"\\" + content_SINGLE: ContentWhere + \\"\\"\\"Return Users where some of the related Contents match this filter\\"\\"\\" + content_SOME: ContentWhere name: String name_CONTAINS: String name_ENDS_WITH: String diff --git a/packages/graphql/tests/schema/experimental-schema/directives/default.test.ts b/packages/graphql/tests/schema/experimental-schema/directives/default.test.ts index fc1f133c4f8..9f2ddcd7c3c 100644 --- a/packages/graphql/tests/schema/experimental-schema/directives/default.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/directives/default.test.ts @@ -215,8 +215,8 @@ describe("@default directive", () => { toBeOverridden: StringAggregateSelectionNonNullable! } - input UserInterfaceImplementationsWhere { - User: UserWhere + enum UserInterfaceImplementation { + User } input UserInterfaceOptions { @@ -237,7 +237,9 @@ describe("@default directive", () => { } input UserInterfaceWhere { - _on: UserInterfaceImplementationsWhere + AND: [UserInterfaceWhere!] + NOT: UserInterfaceWhere + OR: [UserInterfaceWhere!] fromInterface: String fromInterface_CONTAINS: String fromInterface_ENDS_WITH: String @@ -258,6 +260,7 @@ describe("@default directive", () => { toBeOverridden_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") toBeOverridden_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") toBeOverridden_STARTS_WITH: String + typename_IN: [UserInterfaceImplementation!] } input UserOptions { diff --git a/packages/graphql/tests/schema/experimental-schema/directives/filterable.test.ts b/packages/graphql/tests/schema/experimental-schema/directives/filterable.test.ts index f922295804f..bb00512ee19 100644 --- a/packages/graphql/tests/schema/experimental-schema/directives/filterable.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/directives/filterable.test.ts @@ -6656,6 +6656,7 @@ describe("@filterable directive", () => { features: { subscriptions: plugin, }, + experimental: true, }); const schema = await neoSchema.getSchema(); @@ -7054,6 +7055,7 @@ describe("@filterable directive", () => { type Movie { actors(directed: Boolean = true, options: PersonOptions, where: PersonWhere): [Person!]! + actorsAggregate(directed: Boolean = true, where: PersonWhere): MoviePersonActorsAggregationSelection actorsConnection(after: String, directed: Boolean = true, first: Int, sort: [MovieActorsConnectionSort!], where: MovieActorsConnectionWhere): MovieActorsConnection! title: String } @@ -7186,6 +7188,15 @@ describe("@filterable directive", () => { sort: [MovieSort!] } + type MoviePersonActorsAggregationSelection { + count: Int! + node: MoviePersonActorsNodeAggregateSelection + } + + type MoviePersonActorsNodeAggregateSelection { + username: StringAggregateSelectionNonNullable! + } + input MovieRelationInput { actors: [MovieActorsCreateFieldInput!] } @@ -7322,6 +7333,11 @@ describe("@filterable directive", () => { username: String! } + type PersonAggregateSelection { + count: Int! + username: StringAggregateSelectionNonNullable! + } + input PersonConnectInput { _on: PersonImplementationsConnectInput } @@ -7346,6 +7362,10 @@ describe("@filterable directive", () => { username: String! } + enum PersonImplementation { + Actor + } + input PersonImplementationsConnectInput { Actor: [ActorConnectInput!] } @@ -7358,18 +7378,10 @@ describe("@filterable directive", () => { Actor: [ActorDisconnectInput!] } - input PersonImplementationsSubscriptionWhere { - Actor: ActorSubscriptionWhere - } - input PersonImplementationsUpdateInput { Actor: ActorUpdateInput } - input PersonImplementationsWhere { - Actor: ActorWhere - } - input PersonOptions { limit: Int offset: Int @@ -7390,7 +7402,6 @@ describe("@filterable directive", () => { AND: [PersonSubscriptionWhere!] NOT: PersonSubscriptionWhere OR: [PersonSubscriptionWhere!] - _on: PersonImplementationsSubscriptionWhere username: String username_CONTAINS: String username_ENDS_WITH: String @@ -7409,7 +7420,10 @@ describe("@filterable directive", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] + typename_IN: [PersonImplementation!] username: String username_CONTAINS: String username_ENDS_WITH: String @@ -7429,6 +7443,8 @@ describe("@filterable directive", () => { movies(options: MovieOptions, where: MovieWhere): [Movie!]! moviesAggregate(where: MovieWhere): MovieAggregateSelection! moviesConnection(after: String, first: Int, sort: [MovieSort], where: MovieWhere): MoviesConnection! + people(options: PersonOptions, where: PersonWhere): [Person!]! + peopleAggregate(where: PersonWhere): PersonAggregateSelection! } \\"\\"\\"An enum for sorting in either ascending or descending order.\\"\\"\\" diff --git a/packages/graphql/tests/schema/experimental-schema/directives/private.test.ts b/packages/graphql/tests/schema/experimental-schema/directives/private.test.ts index 206cfc3ef0d..a089aa8d6d5 100644 --- a/packages/graphql/tests/schema/experimental-schema/directives/private.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/directives/private.test.ts @@ -146,8 +146,8 @@ describe("@private directive", () => { id: IDAggregateSelectionNullable! } - input UserInterfaceImplementationsWhere { - User: UserWhere + enum UserInterfaceImplementation { + User } input UserInterfaceOptions { @@ -167,7 +167,9 @@ describe("@private directive", () => { } input UserInterfaceWhere { - _on: UserInterfaceImplementationsWhere + AND: [UserInterfaceWhere!] + NOT: UserInterfaceWhere + OR: [UserInterfaceWhere!] id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -178,6 +180,7 @@ describe("@private directive", () => { id_NOT_IN: [ID] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_STARTS_WITH: ID + typename_IN: [UserInterfaceImplementation!] } input UserOptions { diff --git a/packages/graphql/tests/schema/experimental-schema/directives/relationship-aggregate.test.ts b/packages/graphql/tests/schema/experimental-schema/directives/relationship-aggregate.test.ts index 29149e2ff4c..5e41c590628 100644 --- a/packages/graphql/tests/schema/experimental-schema/directives/relationship-aggregate.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/directives/relationship-aggregate.test.ts @@ -1446,12 +1446,12 @@ describe("@relationship directive, aggregate argument", () => { Actor: ActorCreateInput } - input PersonImplementationsUpdateInput { - Actor: ActorUpdateInput + enum PersonImplementation { + Actor } - input PersonImplementationsWhere { - Actor: ActorWhere + input PersonImplementationsUpdateInput { + Actor: ActorUpdateInput } input PersonOptions { @@ -1478,7 +1478,9 @@ describe("@relationship directive, aggregate argument", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] password: String password_CONTAINS: String password_ENDS_WITH: String @@ -1489,6 +1491,7 @@ describe("@relationship directive, aggregate argument", () => { password_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") password_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") password_STARTS_WITH: String + typename_IN: [PersonImplementation!] username: String username_CONTAINS: String username_ENDS_WITH: String @@ -1884,12 +1887,12 @@ describe("@relationship directive, aggregate argument", () => { Actor: ActorCreateInput } - input PersonImplementationsUpdateInput { - Actor: ActorUpdateInput + enum PersonImplementation { + Actor } - input PersonImplementationsWhere { - Actor: ActorWhere + input PersonImplementationsUpdateInput { + Actor: ActorUpdateInput } input PersonOptions { @@ -1916,7 +1919,9 @@ describe("@relationship directive, aggregate argument", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] password: String password_CONTAINS: String password_ENDS_WITH: String @@ -1927,6 +1932,7 @@ describe("@relationship directive, aggregate argument", () => { password_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") password_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") password_STARTS_WITH: String + typename_IN: [PersonImplementation!] username: String username_CONTAINS: String username_ENDS_WITH: String @@ -2332,6 +2338,7 @@ describe("@relationship directive, aggregate argument", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: CastMemberWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -2350,6 +2357,15 @@ describe("@relationship directive, aggregate argument", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related CastMembers match this filter\\"\\"\\" + actors_ALL: CastMemberWhere + \\"\\"\\"Return Movies where none of the related CastMembers match this filter\\"\\"\\" + actors_NONE: CastMemberWhere + actors_NOT: CastMemberWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related CastMembers match this filter\\"\\"\\" + actors_SINGLE: CastMemberWhere + \\"\\"\\"Return Movies where some of the related CastMembers match this filter\\"\\"\\" + actors_SOME: CastMemberWhere title: String title_CONTAINS: String title_ENDS_WITH: String @@ -2855,6 +2871,7 @@ describe("@relationship directive, aggregate argument", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: CastMemberWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -2873,6 +2890,15 @@ describe("@relationship directive, aggregate argument", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related CastMembers match this filter\\"\\"\\" + actors_ALL: CastMemberWhere + \\"\\"\\"Return Movies where none of the related CastMembers match this filter\\"\\"\\" + actors_NONE: CastMemberWhere + actors_NOT: CastMemberWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related CastMembers match this filter\\"\\"\\" + actors_SINGLE: CastMemberWhere + \\"\\"\\"Return Movies where some of the related CastMembers match this filter\\"\\"\\" + actors_SOME: CastMemberWhere title: String title_CONTAINS: String title_ENDS_WITH: String diff --git a/packages/graphql/tests/schema/experimental-schema/directives/relationship-nested-operations.test.ts b/packages/graphql/tests/schema/experimental-schema/directives/relationship-nested-operations.test.ts index e385ee56483..850c7c51181 100644 --- a/packages/graphql/tests/schema/experimental-schema/directives/relationship-nested-operations.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/directives/relationship-nested-operations.test.ts @@ -4180,6 +4180,7 @@ describe("Relationship nested operations", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: PersonWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -4198,6 +4199,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + actors_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + actors_NONE: PersonWhere + actors_NOT: PersonWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + actors_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + actors_SOME: PersonWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -4610,6 +4620,7 @@ describe("Relationship nested operations", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: PersonWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -4628,6 +4639,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + actors_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + actors_NONE: PersonWhere + actors_NOT: PersonWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + actors_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + actors_SOME: PersonWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -5040,6 +5060,7 @@ describe("Relationship nested operations", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: PersonWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -5058,6 +5079,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + actors_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + actors_NONE: PersonWhere + actors_NOT: PersonWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + actors_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + actors_SOME: PersonWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -5455,6 +5485,7 @@ describe("Relationship nested operations", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: PersonWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -5473,6 +5504,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + actors_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + actors_NONE: PersonWhere + actors_NOT: PersonWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + actors_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + actors_SOME: PersonWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -5871,6 +5911,7 @@ describe("Relationship nested operations", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: PersonWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -5889,6 +5930,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + actors_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + actors_NONE: PersonWhere + actors_NOT: PersonWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + actors_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + actors_SOME: PersonWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -6287,6 +6337,7 @@ describe("Relationship nested operations", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: PersonWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -6305,6 +6356,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + actors_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + actors_NONE: PersonWhere + actors_NOT: PersonWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + actors_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + actors_SOME: PersonWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -6671,6 +6731,7 @@ describe("Relationship nested operations", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: PersonWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -6689,6 +6750,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + actors_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + actors_NONE: PersonWhere + actors_NOT: PersonWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + actors_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + actors_SOME: PersonWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -7119,6 +7189,7 @@ describe("Relationship nested operations", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: PersonWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -7137,6 +7208,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + actors_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + actors_NONE: PersonWhere + actors_NOT: PersonWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + actors_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + actors_SOME: PersonWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -7733,6 +7813,7 @@ describe("Relationship nested operations", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: PersonWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -7751,6 +7832,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + actors_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + actors_NONE: PersonWhere + actors_NOT: PersonWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + actors_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + actors_SOME: PersonWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -7761,6 +7851,7 @@ describe("Relationship nested operations", () => { id_NOT_IN: [ID] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_STARTS_WITH: ID + producers: PersonWhere @deprecated(reason: \\"Use \`producers_SOME\` instead.\\") producersConnection: MovieProducersConnectionWhere @deprecated(reason: \\"Use \`producersConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieProducersConnections match this filter @@ -7779,6 +7870,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieProducersConnections match this filter \\"\\"\\" producersConnection_SOME: MovieProducersConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + producers_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + producers_NONE: PersonWhere + producers_NOT: PersonWhere @deprecated(reason: \\"Use \`producers_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + producers_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + producers_SOME: PersonWhere } type MoviesConnection { @@ -8257,6 +8357,7 @@ describe("Relationship nested operations", () => { AND: [MovieWhere!] NOT: MovieWhere OR: [MovieWhere!] + actors: PersonWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -8275,6 +8376,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + actors_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + actors_NONE: PersonWhere + actors_NOT: PersonWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + actors_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + actors_SOME: PersonWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -8285,6 +8395,7 @@ describe("Relationship nested operations", () => { id_NOT_IN: [ID] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_STARTS_WITH: ID + producers: PersonWhere @deprecated(reason: \\"Use \`producers_SOME\` instead.\\") producersConnection: MovieProducersConnectionWhere @deprecated(reason: \\"Use \`producersConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieProducersConnections match this filter @@ -8303,6 +8414,15 @@ describe("Relationship nested operations", () => { Return Movies where some of the related MovieProducersConnections match this filter \\"\\"\\" producersConnection_SOME: MovieProducersConnectionWhere + \\"\\"\\"Return Movies where all of the related People match this filter\\"\\"\\" + producers_ALL: PersonWhere + \\"\\"\\"Return Movies where none of the related People match this filter\\"\\"\\" + producers_NONE: PersonWhere + producers_NOT: PersonWhere @deprecated(reason: \\"Use \`producers_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related People match this filter\\"\\"\\" + producers_SINGLE: PersonWhere + \\"\\"\\"Return Movies where some of the related People match this filter\\"\\"\\" + producers_SOME: PersonWhere } type MoviesConnection { @@ -8734,9 +8854,9 @@ describe("Relationship nested operations", () => { name: StringAggregateSelectionNullable! } - input PersonImplementationsWhere { - PersonOne: PersonOneWhere - PersonTwo: PersonTwoWhere + enum PersonImplementation { + PersonOne + PersonTwo } type PersonOne implements Person { @@ -8885,7 +9005,9 @@ describe("Relationship nested operations", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] name: String name_CONTAINS: String name_ENDS_WITH: String @@ -8896,6 +9018,7 @@ describe("Relationship nested operations", () => { name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [PersonImplementation!] } type Query { @@ -9186,9 +9309,9 @@ describe("Relationship nested operations", () => { PersonTwo: PersonTwoCreateInput } - input PersonImplementationsWhere { - PersonOne: PersonOneWhere - PersonTwo: PersonTwoWhere + enum PersonImplementation { + PersonOne + PersonTwo } type PersonOne implements Person { @@ -9337,7 +9460,9 @@ describe("Relationship nested operations", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] name: String name_CONTAINS: String name_ENDS_WITH: String @@ -9348,6 +9473,7 @@ describe("Relationship nested operations", () => { name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [PersonImplementation!] } type Query { @@ -9637,9 +9763,9 @@ describe("Relationship nested operations", () => { node: PersonWhere! } - input PersonImplementationsWhere { - PersonOne: PersonOneWhere - PersonTwo: PersonTwoWhere + enum PersonImplementation { + PersonOne + PersonTwo } type PersonOne implements Person { @@ -9788,7 +9914,9 @@ describe("Relationship nested operations", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] name: String name_CONTAINS: String name_ENDS_WITH: String @@ -9799,6 +9927,7 @@ describe("Relationship nested operations", () => { name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [PersonImplementation!] } type Query { @@ -10075,16 +10204,16 @@ describe("Relationship nested operations", () => { name: StringAggregateSelectionNullable! } + enum PersonImplementation { + PersonOne + PersonTwo + } + input PersonImplementationsUpdateInput { PersonOne: PersonOneUpdateInput PersonTwo: PersonTwoUpdateInput } - input PersonImplementationsWhere { - PersonOne: PersonOneWhere - PersonTwo: PersonTwoWhere - } - type PersonOne implements Person { name: String someExtraProp: [Int!]! @@ -10236,7 +10365,9 @@ describe("Relationship nested operations", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] name: String name_CONTAINS: String name_ENDS_WITH: String @@ -10247,6 +10378,7 @@ describe("Relationship nested operations", () => { name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [PersonImplementation!] } type Query { @@ -10527,9 +10659,9 @@ describe("Relationship nested operations", () => { name: StringAggregateSelectionNullable! } - input PersonImplementationsWhere { - PersonOne: PersonOneWhere - PersonTwo: PersonTwoWhere + enum PersonImplementation { + PersonOne + PersonTwo } type PersonOne implements Person { @@ -10678,7 +10810,9 @@ describe("Relationship nested operations", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] name: String name_CONTAINS: String name_ENDS_WITH: String @@ -10689,6 +10823,7 @@ describe("Relationship nested operations", () => { name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [PersonImplementation!] } type Query { @@ -10969,9 +11104,9 @@ describe("Relationship nested operations", () => { name: StringAggregateSelectionNullable! } - input PersonImplementationsWhere { - PersonOne: PersonOneWhere - PersonTwo: PersonTwoWhere + enum PersonImplementation { + PersonOne + PersonTwo } type PersonOne implements Person { @@ -11120,7 +11255,9 @@ describe("Relationship nested operations", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] name: String name_CONTAINS: String name_ENDS_WITH: String @@ -11131,6 +11268,7 @@ describe("Relationship nested operations", () => { name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [PersonImplementation!] } type Query { @@ -11523,16 +11661,16 @@ describe("Relationship nested operations", () => { PersonTwo: PersonTwoCreateInput } + enum PersonImplementation { + PersonOne + PersonTwo + } + input PersonImplementationsUpdateInput { PersonOne: PersonOneUpdateInput PersonTwo: PersonTwoUpdateInput } - input PersonImplementationsWhere { - PersonOne: PersonOneWhere - PersonTwo: PersonTwoWhere - } - type PersonOne implements Person { name: String someExtraProp: [Int!]! @@ -11684,7 +11822,9 @@ describe("Relationship nested operations", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] name: String name_CONTAINS: String name_ENDS_WITH: String @@ -11695,6 +11835,7 @@ describe("Relationship nested operations", () => { name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [PersonImplementation!] } type Query { @@ -12063,9 +12204,9 @@ describe("Relationship nested operations", () => { PersonTwo: PersonTwoCreateInput } - input PersonImplementationsWhere { - PersonOne: PersonOneWhere - PersonTwo: PersonTwoWhere + enum PersonImplementation { + PersonOne + PersonTwo } type PersonOne implements Person { @@ -12214,7 +12355,9 @@ describe("Relationship nested operations", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] name: String name_CONTAINS: String name_ENDS_WITH: String @@ -12225,6 +12368,7 @@ describe("Relationship nested operations", () => { name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [PersonImplementation!] } type Query { diff --git a/packages/graphql/tests/schema/experimental-schema/directives/selectable.test.ts b/packages/graphql/tests/schema/experimental-schema/directives/selectable.test.ts index 8ddaecbc82d..afaa4a028d9 100644 --- a/packages/graphql/tests/schema/experimental-schema/directives/selectable.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/directives/selectable.test.ts @@ -1878,6 +1878,7 @@ describe("@selectable", () => { AND: [ActorWhere!] NOT: ActorWhere OR: [ActorWhere!] + actedIn: ProductionWhere @deprecated(reason: \\"Use \`actedIn_SOME\` instead.\\") actedInConnection: ActorActedInConnectionWhere @deprecated(reason: \\"Use \`actedInConnection_SOME\` instead.\\") \\"\\"\\" Return Actors where all of the related ActorActedInConnections match this filter @@ -1896,6 +1897,15 @@ describe("@selectable", () => { Return Actors where some of the related ActorActedInConnections match this filter \\"\\"\\" actedInConnection_SOME: ActorActedInConnectionWhere + \\"\\"\\"Return Actors where all of the related Productions match this filter\\"\\"\\" + actedIn_ALL: ProductionWhere + \\"\\"\\"Return Actors where none of the related Productions match this filter\\"\\"\\" + actedIn_NONE: ProductionWhere + actedIn_NOT: ProductionWhere @deprecated(reason: \\"Use \`actedIn_NONE\` instead.\\") + \\"\\"\\"Return Actors where one of the related Productions match this filter\\"\\"\\" + actedIn_SINGLE: ProductionWhere + \\"\\"\\"Return Actors where some of the related Productions match this filter\\"\\"\\" + actedIn_SOME: ProductionWhere name: String name_CONTAINS: String name_ENDS_WITH: String @@ -2417,6 +2427,7 @@ describe("@selectable", () => { AND: [ActorWhere!] NOT: ActorWhere OR: [ActorWhere!] + actedIn: ProductionWhere @deprecated(reason: \\"Use \`actedIn_SOME\` instead.\\") actedInConnection: ActorActedInConnectionWhere @deprecated(reason: \\"Use \`actedInConnection_SOME\` instead.\\") \\"\\"\\" Return Actors where all of the related ActorActedInConnections match this filter @@ -2435,6 +2446,15 @@ describe("@selectable", () => { Return Actors where some of the related ActorActedInConnections match this filter \\"\\"\\" actedInConnection_SOME: ActorActedInConnectionWhere + \\"\\"\\"Return Actors where all of the related Productions match this filter\\"\\"\\" + actedIn_ALL: ProductionWhere + \\"\\"\\"Return Actors where none of the related Productions match this filter\\"\\"\\" + actedIn_NONE: ProductionWhere + actedIn_NOT: ProductionWhere @deprecated(reason: \\"Use \`actedIn_NONE\` instead.\\") + \\"\\"\\"Return Actors where one of the related Productions match this filter\\"\\"\\" + actedIn_SINGLE: ProductionWhere + \\"\\"\\"Return Actors where some of the related Productions match this filter\\"\\"\\" + actedIn_SOME: ProductionWhere name: String name_CONTAINS: String name_ENDS_WITH: String @@ -3067,16 +3087,16 @@ describe("@selectable", () => { Series: SeriesCreateInput } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -3101,7 +3121,9 @@ describe("@selectable", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] description: String description_CONTAINS: String description_ENDS_WITH: String @@ -3122,6 +3144,7 @@ describe("@selectable", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -3604,16 +3627,16 @@ describe("@selectable", () => { Series: SeriesCreateInput } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -3638,7 +3661,9 @@ describe("@selectable", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] description: String description_CONTAINS: String description_ENDS_WITH: String @@ -3659,6 +3684,7 @@ describe("@selectable", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { diff --git a/packages/graphql/tests/schema/experimental-schema/directives/settable.test.ts b/packages/graphql/tests/schema/experimental-schema/directives/settable.test.ts index 9c398fa7fa5..bec896df0d4 100644 --- a/packages/graphql/tests/schema/experimental-schema/directives/settable.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/directives/settable.test.ts @@ -2989,6 +2989,7 @@ describe("@settable", () => { AND: [ActorWhere!] NOT: ActorWhere OR: [ActorWhere!] + actedIn: ProductionWhere @deprecated(reason: \\"Use \`actedIn_SOME\` instead.\\") actedInConnection: ActorActedInConnectionWhere @deprecated(reason: \\"Use \`actedInConnection_SOME\` instead.\\") \\"\\"\\" Return Actors where all of the related ActorActedInConnections match this filter @@ -3007,6 +3008,15 @@ describe("@settable", () => { Return Actors where some of the related ActorActedInConnections match this filter \\"\\"\\" actedInConnection_SOME: ActorActedInConnectionWhere + \\"\\"\\"Return Actors where all of the related Productions match this filter\\"\\"\\" + actedIn_ALL: ProductionWhere + \\"\\"\\"Return Actors where none of the related Productions match this filter\\"\\"\\" + actedIn_NONE: ProductionWhere + actedIn_NOT: ProductionWhere @deprecated(reason: \\"Use \`actedIn_NONE\` instead.\\") + \\"\\"\\"Return Actors where one of the related Productions match this filter\\"\\"\\" + actedIn_SINGLE: ProductionWhere + \\"\\"\\"Return Actors where some of the related Productions match this filter\\"\\"\\" + actedIn_SOME: ProductionWhere name: String name_CONTAINS: String name_ENDS_WITH: String @@ -3497,6 +3507,7 @@ describe("@settable", () => { AND: [ActorWhere!] NOT: ActorWhere OR: [ActorWhere!] + actedIn: ProductionWhere @deprecated(reason: \\"Use \`actedIn_SOME\` instead.\\") actedInConnection: ActorActedInConnectionWhere @deprecated(reason: \\"Use \`actedInConnection_SOME\` instead.\\") \\"\\"\\" Return Actors where all of the related ActorActedInConnections match this filter @@ -3515,6 +3526,15 @@ describe("@settable", () => { Return Actors where some of the related ActorActedInConnections match this filter \\"\\"\\" actedInConnection_SOME: ActorActedInConnectionWhere + \\"\\"\\"Return Actors where all of the related Productions match this filter\\"\\"\\" + actedIn_ALL: ProductionWhere + \\"\\"\\"Return Actors where none of the related Productions match this filter\\"\\"\\" + actedIn_NONE: ProductionWhere + actedIn_NOT: ProductionWhere @deprecated(reason: \\"Use \`actedIn_NONE\` instead.\\") + \\"\\"\\"Return Actors where one of the related Productions match this filter\\"\\"\\" + actedIn_SINGLE: ProductionWhere + \\"\\"\\"Return Actors where some of the related Productions match this filter\\"\\"\\" + actedIn_SOME: ProductionWhere name: String name_CONTAINS: String name_ENDS_WITH: String @@ -4011,6 +4031,7 @@ describe("@settable", () => { AND: [ActorWhere!] NOT: ActorWhere OR: [ActorWhere!] + actedIn: ProductionWhere @deprecated(reason: \\"Use \`actedIn_SOME\` instead.\\") actedInConnection: ActorActedInConnectionWhere @deprecated(reason: \\"Use \`actedInConnection_SOME\` instead.\\") \\"\\"\\" Return Actors where all of the related ActorActedInConnections match this filter @@ -4029,6 +4050,15 @@ describe("@settable", () => { Return Actors where some of the related ActorActedInConnections match this filter \\"\\"\\" actedInConnection_SOME: ActorActedInConnectionWhere + \\"\\"\\"Return Actors where all of the related Productions match this filter\\"\\"\\" + actedIn_ALL: ProductionWhere + \\"\\"\\"Return Actors where none of the related Productions match this filter\\"\\"\\" + actedIn_NONE: ProductionWhere + actedIn_NOT: ProductionWhere @deprecated(reason: \\"Use \`actedIn_NONE\` instead.\\") + \\"\\"\\"Return Actors where one of the related Productions match this filter\\"\\"\\" + actedIn_SINGLE: ProductionWhere + \\"\\"\\"Return Actors where some of the related Productions match this filter\\"\\"\\" + actedIn_SOME: ProductionWhere name: String name_CONTAINS: String name_ENDS_WITH: String @@ -4717,6 +4747,7 @@ describe("@settable", () => { AND: [ActorWhere!] NOT: ActorWhere OR: [ActorWhere!] + actedIn: ProductionWhere @deprecated(reason: \\"Use \`actedIn_SOME\` instead.\\") actedInConnection: ActorActedInConnectionWhere @deprecated(reason: \\"Use \`actedInConnection_SOME\` instead.\\") \\"\\"\\" Return Actors where all of the related ActorActedInConnections match this filter @@ -4735,6 +4766,15 @@ describe("@settable", () => { Return Actors where some of the related ActorActedInConnections match this filter \\"\\"\\" actedInConnection_SOME: ActorActedInConnectionWhere + \\"\\"\\"Return Actors where all of the related Productions match this filter\\"\\"\\" + actedIn_ALL: ProductionWhere + \\"\\"\\"Return Actors where none of the related Productions match this filter\\"\\"\\" + actedIn_NONE: ProductionWhere + actedIn_NOT: ProductionWhere @deprecated(reason: \\"Use \`actedIn_NONE\` instead.\\") + \\"\\"\\"Return Actors where one of the related Productions match this filter\\"\\"\\" + actedIn_SINGLE: ProductionWhere + \\"\\"\\"Return Actors where some of the related Productions match this filter\\"\\"\\" + actedIn_SOME: ProductionWhere name: String name_CONTAINS: String name_ENDS_WITH: String @@ -5554,16 +5594,16 @@ describe("@settable", () => { Series: SeriesCreateInput } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -5588,7 +5628,9 @@ describe("@settable", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] description: String description_CONTAINS: String description_ENDS_WITH: String @@ -5609,6 +5651,7 @@ describe("@settable", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -6078,9 +6121,9 @@ describe("@settable", () => { Series: SeriesCreateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere + enum ProductionImplementation { + Movie + Series } input ProductionOptions { @@ -6101,7 +6144,9 @@ describe("@settable", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] description: String description_CONTAINS: String description_ENDS_WITH: String @@ -6122,6 +6167,7 @@ describe("@settable", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -6845,6 +6891,11 @@ describe("@settable", () => { actors: [ProductionActorsDisconnectFieldInput!] } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsConnectInput { Movie: [MovieConnectInput!] Series: [SeriesConnectInput!] @@ -6860,11 +6911,6 @@ describe("@settable", () => { Series: [SeriesDisconnectInput!] } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -6883,7 +6929,9 @@ describe("@settable", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] actors: ActorWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsAggregate: ProductionActorsAggregateInput actorsConnection: ProductionActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") @@ -6933,6 +6981,7 @@ describe("@settable", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -7776,6 +7825,11 @@ describe("@settable", () => { actors: [ProductionActorsDisconnectFieldInput!] } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsConnectInput { Movie: [MovieConnectInput!] Series: [SeriesConnectInput!] @@ -7796,11 +7850,6 @@ describe("@settable", () => { Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -7826,7 +7875,9 @@ describe("@settable", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] actors: ActorWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsAggregate: ProductionActorsAggregateInput actorsConnection: ProductionActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") @@ -7876,6 +7927,7 @@ describe("@settable", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { diff --git a/packages/graphql/tests/schema/experimental-schema/inheritance.test.ts b/packages/graphql/tests/schema/experimental-schema/inheritance.test.ts index 3c15467c57a..31a123f91b5 100644 --- a/packages/graphql/tests/schema/experimental-schema/inheritance.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/inheritance.test.ts @@ -385,6 +385,10 @@ describe("inheritance", () => { where: PersonFriendsConnectionWhere } + enum PersonImplementation { + Actor + } + input PersonImplementationsConnectInput { Actor: [ActorConnectInput!] } @@ -401,10 +405,6 @@ describe("inheritance", () => { Actor: ActorUpdateInput } - input PersonImplementationsWhere { - Actor: ActorWhere - } - input PersonOptions { limit: Int offset: Int @@ -428,7 +428,9 @@ describe("inheritance", () => { } input PersonWhere { - _on: PersonImplementationsWhere + AND: [PersonWhere!] + NOT: PersonWhere + OR: [PersonWhere!] friendsConnection: PersonFriendsConnectionWhere @deprecated(reason: \\"Use \`friendsConnection_SOME\` instead.\\") \\"\\"\\" Return People where all of the related PersonFriendsConnections match this filter @@ -457,6 +459,7 @@ describe("inheritance", () => { name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [PersonImplementation!] } type Query { diff --git a/packages/graphql/tests/schema/experimental-schema/interface-relationships.test.ts b/packages/graphql/tests/schema/experimental-schema/interface-relationships.test.ts index da9f2096d89..f8e7a944142 100644 --- a/packages/graphql/tests/schema/experimental-schema/interface-relationships.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/interface-relationships.test.ts @@ -419,16 +419,16 @@ describe("Interface Relationships", () => { Series: SeriesCreateInput } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -451,7 +451,9 @@ describe("Interface Relationships", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] title: String title_CONTAINS: String title_ENDS_WITH: String @@ -462,6 +464,7 @@ describe("Interface Relationships", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -1578,6 +1581,11 @@ describe("Interface Relationships", () => { actors: [ProductionActorsDisconnectFieldInput!] } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsConnectInput { Movie: [MovieConnectInput!] Series: [SeriesConnectInput!] @@ -1598,11 +1606,6 @@ describe("Interface Relationships", () => { Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -1626,7 +1629,9 @@ describe("Interface Relationships", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] actors: ActorWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsAggregate: ProductionActorsAggregateInput actorsConnection: ProductionActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") @@ -1666,6 +1671,7 @@ describe("Interface Relationships", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -2235,6 +2241,11 @@ describe("Interface Relationships", () => { interface2: [Interface1Interface2DisconnectFieldInput!] } + enum Interface1Implementation { + Type1Interface1 + Type2Interface1 + } + input Interface1ImplementationsConnectInput { Type1Interface1: [Type1Interface1ConnectInput!] Type2Interface1: [Type2Interface1ConnectInput!] @@ -2255,11 +2266,6 @@ describe("Interface Relationships", () => { Type2Interface1: Type2Interface1UpdateInput } - input Interface1ImplementationsWhere { - Type1Interface1: Type1Interface1Where - Type2Interface1: Type2Interface1Where - } - input Interface1Interface2ConnectFieldInput { where: Interface2ConnectWhere } @@ -2340,7 +2346,9 @@ describe("Interface Relationships", () => { } input Interface1Where { - _on: Interface1ImplementationsWhere + AND: [Interface1Where!] + NOT: Interface1Where + OR: [Interface1Where!] field1: String field1_CONTAINS: String field1_ENDS_WITH: String @@ -2369,6 +2377,7 @@ describe("Interface Relationships", () => { Return Interface1s where some of the related Interface1Interface2Connections match this filter \\"\\"\\" interface2Connection_SOME: Interface1Interface2ConnectionWhere + typename_IN: [Interface1Implementation!] } interface Interface2 { @@ -2389,16 +2398,16 @@ describe("Interface Relationships", () => { Type2Interface2: Type2Interface2CreateInput } + enum Interface2Implementation { + Type1Interface2 + Type2Interface2 + } + input Interface2ImplementationsUpdateInput { Type1Interface2: Type1Interface2UpdateInput Type2Interface2: Type2Interface2UpdateInput } - input Interface2ImplementationsWhere { - Type1Interface2: Type1Interface2Where - Type2Interface2: Type2Interface2Where - } - input Interface2Options { limit: Int offset: Int @@ -2421,7 +2430,9 @@ describe("Interface Relationships", () => { } input Interface2Where { - _on: Interface2ImplementationsWhere + AND: [Interface2Where!] + NOT: Interface2Where + OR: [Interface2Where!] field2: String field2_CONTAINS: String field2_ENDS_WITH: String @@ -2432,6 +2443,7 @@ describe("Interface Relationships", () => { field2_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") field2_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") field2_STARTS_WITH: String + typename_IN: [Interface2Implementation!] } type Mutation { @@ -3609,6 +3621,11 @@ describe("Interface Relationships", () => { creator: ContentCreatorDisconnectFieldInput } + enum ContentImplementation { + Comment + Post + } + input ContentImplementationsConnectInput { Comment: [CommentConnectInput!] Post: [PostConnectInput!] @@ -3629,11 +3646,6 @@ describe("Interface Relationships", () => { Post: PostUpdateInput } - input ContentImplementationsWhere { - Comment: CommentWhere - Post: PostWhere - } - input ContentOptions { limit: Int offset: Int @@ -3659,7 +3671,9 @@ describe("Interface Relationships", () => { } input ContentWhere { - _on: ContentImplementationsWhere + AND: [ContentWhere!] + NOT: ContentWhere + OR: [ContentWhere!] content: String content_CONTAINS: String content_ENDS_WITH: String @@ -3685,6 +3699,7 @@ describe("Interface Relationships", () => { id_NOT_IN: [ID] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_STARTS_WITH: ID + typename_IN: [ContentImplementation!] } type CreateCommentsMutationResponse { diff --git a/packages/graphql/tests/schema/experimental-schema/interfaces.test.ts b/packages/graphql/tests/schema/experimental-schema/interfaces.test.ts index c89e1dcda29..e26b066f4e5 100644 --- a/packages/graphql/tests/schema/experimental-schema/interfaces.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/interfaces.test.ts @@ -162,8 +162,8 @@ describe("Interfaces", () => { id: IDAggregateSelectionNullable! } - input MovieNodeImplementationsWhere { - Movie: MovieWhere + enum MovieNodeImplementation { + Movie } input MovieNodeMoviesConnectFieldInput { @@ -247,7 +247,9 @@ describe("Interfaces", () => { } input MovieNodeWhere { - _on: MovieNodeImplementationsWhere + AND: [MovieNodeWhere!] + NOT: MovieNodeWhere + OR: [MovieNodeWhere!] id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -258,6 +260,7 @@ describe("Interfaces", () => { id_NOT_IN: [ID] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_STARTS_WITH: ID + typename_IN: [MovieNodeImplementation!] } input MovieOptions { @@ -526,8 +529,8 @@ describe("Interfaces", () => { id: IDAggregateSelectionNullable! } - input MovieNodeImplementationsWhere { - Movie: MovieWhere + enum MovieNodeImplementation { + Movie } input MovieNodeMoviesConnectFieldInput { @@ -611,7 +614,9 @@ describe("Interfaces", () => { } input MovieNodeWhere { - _on: MovieNodeImplementationsWhere + AND: [MovieNodeWhere!] + NOT: MovieNodeWhere + OR: [MovieNodeWhere!] id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -622,6 +627,7 @@ describe("Interfaces", () => { id_NOT_IN: [ID] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_STARTS_WITH: ID + typename_IN: [MovieNodeImplementation!] } input MovieOptions { diff --git a/packages/graphql/tests/schema/experimental-schema/interfaces/aggregations.test.ts b/packages/graphql/tests/schema/experimental-schema/interfaces/aggregations.test.ts index 0d7f166d5fd..6c39982c0a9 100644 --- a/packages/graphql/tests/schema/experimental-schema/interfaces/aggregations.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/interfaces/aggregations.test.ts @@ -199,8 +199,8 @@ describe("Interface Top Level Aggregations", () => { title: StringAggregateSelectionNonNullable! } - input ProductionImplementationsWhere { - Movie: MovieWhere + enum ProductionImplementation { + Movie } input ProductionOptions { @@ -221,7 +221,9 @@ describe("Interface Top Level Aggregations", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] cost: Float cost_GT: Float cost_GTE: Float @@ -240,6 +242,7 @@ describe("Interface Top Level Aggregations", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -707,16 +710,16 @@ describe("Interface Top Level Aggregations", () => { Series: SeriesCreateInput } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -745,7 +748,9 @@ describe("Interface Top Level Aggregations", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] cost: Float cost_GT: Float cost_GTE: Float @@ -764,6 +769,7 @@ describe("Interface Top Level Aggregations", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { diff --git a/packages/graphql/tests/schema/experimental-schema/interfaces/typename-in.test.ts b/packages/graphql/tests/schema/experimental-schema/interfaces/typename-in.test.ts new file mode 100644 index 00000000000..55fbe71a7f6 --- /dev/null +++ b/packages/graphql/tests/schema/experimental-schema/interfaces/typename-in.test.ts @@ -0,0 +1,611 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { gql } from "graphql-tag"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../../src"; + +describe("typename_IN", () => { + test("typename_IN", async () => { + const typeDefs = gql` + interface Production { + title: String! + cost: Float! + } + + type Movie implements Production { + title: String! + cost: Float! + runtime: Int! + } + + type Series implements Production { + title: String! + cost: Float! + episodes: Int! + } + + type Actor { + name: String! + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + const neoSchema = new Neo4jGraphQL({ typeDefs, experimental: true }); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + type Actor { + actedIn(directed: Boolean = true, options: ProductionOptions, where: ProductionWhere): [Production!]! + actedInAggregate(directed: Boolean = true, where: ProductionWhere): ActorProductionActedInAggregationSelection + actedInConnection(after: String, directed: Boolean = true, first: Int, sort: [ActorActedInConnectionSort!], where: ActorActedInConnectionWhere): ActorActedInConnection! + name: String! + } + + input ActorActedInConnectFieldInput { + where: ProductionConnectWhere + } + + type ActorActedInConnection { + edges: [ActorActedInRelationship!]! + pageInfo: PageInfo! + totalCount: Int! + } + + input ActorActedInConnectionSort { + node: ProductionSort + } + + input ActorActedInConnectionWhere { + AND: [ActorActedInConnectionWhere!] + NOT: ActorActedInConnectionWhere + OR: [ActorActedInConnectionWhere!] + node: ProductionWhere + node_NOT: ProductionWhere @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + } + + input ActorActedInCreateFieldInput { + node: ProductionCreateInput! + } + + input ActorActedInDeleteFieldInput { + where: ActorActedInConnectionWhere + } + + input ActorActedInDisconnectFieldInput { + where: ActorActedInConnectionWhere + } + + input ActorActedInFieldInput { + connect: [ActorActedInConnectFieldInput!] + create: [ActorActedInCreateFieldInput!] + } + + type ActorActedInRelationship { + cursor: String! + node: Production! + } + + input ActorActedInUpdateConnectionInput { + node: ProductionUpdateInput + } + + input ActorActedInUpdateFieldInput { + connect: [ActorActedInConnectFieldInput!] + create: [ActorActedInCreateFieldInput!] + delete: [ActorActedInDeleteFieldInput!] + disconnect: [ActorActedInDisconnectFieldInput!] + update: ActorActedInUpdateConnectionInput + where: ActorActedInConnectionWhere + } + + type ActorAggregateSelection { + count: Int! + name: StringAggregateSelectionNonNullable! + } + + input ActorConnectInput { + actedIn: [ActorActedInConnectFieldInput!] + } + + input ActorCreateInput { + actedIn: ActorActedInFieldInput + name: String! + } + + input ActorDeleteInput { + actedIn: [ActorActedInDeleteFieldInput!] + } + + input ActorDisconnectInput { + actedIn: [ActorActedInDisconnectFieldInput!] + } + + type ActorEdge { + cursor: String! + node: Actor! + } + + input ActorOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more ActorSort objects to sort Actors by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [ActorSort!] + } + + type ActorProductionActedInAggregationSelection { + count: Int! + node: ActorProductionActedInNodeAggregateSelection + } + + type ActorProductionActedInNodeAggregateSelection { + cost: FloatAggregateSelectionNonNullable! + title: StringAggregateSelectionNonNullable! + } + + input ActorRelationInput { + actedIn: [ActorActedInCreateFieldInput!] + } + + \\"\\"\\" + Fields to sort Actors by. The order in which sorts are applied is not guaranteed when specifying many fields in one ActorSort object. + \\"\\"\\" + input ActorSort { + name: SortDirection + } + + input ActorUpdateInput { + actedIn: [ActorActedInUpdateFieldInput!] + name: String + } + + input ActorWhere { + AND: [ActorWhere!] + NOT: ActorWhere + OR: [ActorWhere!] + actedInConnection: ActorActedInConnectionWhere @deprecated(reason: \\"Use \`actedInConnection_SOME\` instead.\\") + \\"\\"\\" + Return Actors where all of the related ActorActedInConnections match this filter + \\"\\"\\" + actedInConnection_ALL: ActorActedInConnectionWhere + \\"\\"\\" + Return Actors where none of the related ActorActedInConnections match this filter + \\"\\"\\" + actedInConnection_NONE: ActorActedInConnectionWhere + actedInConnection_NOT: ActorActedInConnectionWhere @deprecated(reason: \\"Use \`actedInConnection_NONE\` instead.\\") + \\"\\"\\" + Return Actors where one of the related ActorActedInConnections match this filter + \\"\\"\\" + actedInConnection_SINGLE: ActorActedInConnectionWhere + \\"\\"\\" + Return Actors where some of the related ActorActedInConnections match this filter + \\"\\"\\" + actedInConnection_SOME: ActorActedInConnectionWhere + name: String + name_CONTAINS: String + name_ENDS_WITH: String + name_IN: [String!] + name_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_STARTS_WITH: String + } + + type ActorsConnection { + edges: [ActorEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + type CreateActorsMutationResponse { + actors: [Actor!]! + info: CreateInfo! + } + + \\"\\"\\" + Information about the number of nodes and relationships created during a create mutation + \\"\\"\\" + type CreateInfo { + bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") + nodesCreated: Int! + relationshipsCreated: Int! + } + + type CreateMoviesMutationResponse { + info: CreateInfo! + movies: [Movie!]! + } + + type CreateSeriesMutationResponse { + info: CreateInfo! + series: [Series!]! + } + + \\"\\"\\" + Information about the number of nodes and relationships deleted during a delete mutation + \\"\\"\\" + type DeleteInfo { + bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type FloatAggregateSelectionNonNullable { + average: Float! + max: Float! + min: Float! + sum: Float! + } + + type IntAggregateSelectionNonNullable { + average: Float! + max: Int! + min: Int! + sum: Int! + } + + type Movie implements Production { + cost: Float! + runtime: Int! + title: String! + } + + type MovieAggregateSelection { + cost: FloatAggregateSelectionNonNullable! + count: Int! + runtime: IntAggregateSelectionNonNullable! + title: StringAggregateSelectionNonNullable! + } + + input MovieCreateInput { + cost: Float! + runtime: Int! + title: String! + } + + type MovieEdge { + cursor: String! + node: Movie! + } + + input MovieOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more MovieSort objects to sort Movies by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [MovieSort!] + } + + \\"\\"\\" + Fields to sort Movies by. The order in which sorts are applied is not guaranteed when specifying many fields in one MovieSort object. + \\"\\"\\" + input MovieSort { + cost: SortDirection + runtime: SortDirection + title: SortDirection + } + + input MovieUpdateInput { + cost: Float + cost_ADD: Float + cost_DIVIDE: Float + cost_MULTIPLY: Float + cost_SUBTRACT: Float + runtime: Int + runtime_DECREMENT: Int + runtime_INCREMENT: Int + title: String + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + cost: Float + cost_GT: Float + cost_GTE: Float + cost_IN: [Float!] + cost_LT: Float + cost_LTE: Float + cost_NOT: Float @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + cost_NOT_IN: [Float!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + runtime: Int + runtime_GT: Int + runtime_GTE: Int + runtime_IN: [Int!] + runtime_LT: Int + runtime_LTE: Int + runtime_NOT: Int @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + runtime_NOT_IN: [Int!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title: String + title_CONTAINS: String + title_ENDS_WITH: String + title_IN: [String!] + title_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_STARTS_WITH: String + } + + type MoviesConnection { + edges: [MovieEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + type Mutation { + createActors(input: [ActorCreateInput!]!): CreateActorsMutationResponse! + createMovies(input: [MovieCreateInput!]!): CreateMoviesMutationResponse! + createSeries(input: [SeriesCreateInput!]!): CreateSeriesMutationResponse! + deleteActors(delete: ActorDeleteInput, where: ActorWhere): DeleteInfo! + deleteMovies(where: MovieWhere): DeleteInfo! + deleteSeries(where: SeriesWhere): DeleteInfo! + updateActors(connect: ActorConnectInput, create: ActorRelationInput, delete: ActorDeleteInput, disconnect: ActorDisconnectInput, update: ActorUpdateInput, where: ActorWhere): UpdateActorsMutationResponse! + updateMovies(update: MovieUpdateInput, where: MovieWhere): UpdateMoviesMutationResponse! + updateSeries(update: SeriesUpdateInput, where: SeriesWhere): UpdateSeriesMutationResponse! + } + + \\"\\"\\"Pagination information (Relay)\\"\\"\\" + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + interface Production { + cost: Float! + title: String! + } + + type ProductionAggregateSelection { + cost: FloatAggregateSelectionNonNullable! + count: Int! + title: StringAggregateSelectionNonNullable! + } + + input ProductionConnectWhere { + node: ProductionWhere! + } + + input ProductionCreateInput { + Movie: MovieCreateInput + Series: SeriesCreateInput + } + + enum ProductionImplementation { + Movie + Series + } + + input ProductionImplementationsUpdateInput { + Movie: MovieUpdateInput + Series: SeriesUpdateInput + } + + input ProductionOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more ProductionSort objects to sort Productions by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [ProductionSort] + } + + \\"\\"\\" + Fields to sort Productions by. The order in which sorts are applied is not guaranteed when specifying many fields in one ProductionSort object. + \\"\\"\\" + input ProductionSort { + cost: SortDirection + title: SortDirection + } + + input ProductionUpdateInput { + _on: ProductionImplementationsUpdateInput + cost: Float + cost_ADD: Float + cost_DIVIDE: Float + cost_MULTIPLY: Float + cost_SUBTRACT: Float + title: String + } + + input ProductionWhere { + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] + cost: Float + cost_GT: Float + cost_GTE: Float + cost_IN: [Float!] + cost_LT: Float + cost_LTE: Float + cost_NOT: Float @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + cost_NOT_IN: [Float!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title: String + title_CONTAINS: String + title_ENDS_WITH: String + title_IN: [String!] + title_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] + } + + type Query { + actors(options: ActorOptions, where: ActorWhere): [Actor!]! + actorsAggregate(where: ActorWhere): ActorAggregateSelection! + actorsConnection(after: String, first: Int, sort: [ActorSort], where: ActorWhere): ActorsConnection! + movies(options: MovieOptions, where: MovieWhere): [Movie!]! + moviesAggregate(where: MovieWhere): MovieAggregateSelection! + moviesConnection(after: String, first: Int, sort: [MovieSort], where: MovieWhere): MoviesConnection! + productions(options: ProductionOptions, where: ProductionWhere): [Production!]! + productionsAggregate(where: ProductionWhere): ProductionAggregateSelection! + series(options: SeriesOptions, where: SeriesWhere): [Series!]! + seriesAggregate(where: SeriesWhere): SeriesAggregateSelection! + seriesConnection(after: String, first: Int, sort: [SeriesSort], where: SeriesWhere): SeriesConnection! + } + + type Series implements Production { + cost: Float! + episodes: Int! + title: String! + } + + type SeriesAggregateSelection { + cost: FloatAggregateSelectionNonNullable! + count: Int! + episodes: IntAggregateSelectionNonNullable! + title: StringAggregateSelectionNonNullable! + } + + type SeriesConnection { + edges: [SeriesEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + input SeriesCreateInput { + cost: Float! + episodes: Int! + title: String! + } + + type SeriesEdge { + cursor: String! + node: Series! + } + + input SeriesOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more SeriesSort objects to sort Series by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [SeriesSort!] + } + + \\"\\"\\" + Fields to sort Series by. The order in which sorts are applied is not guaranteed when specifying many fields in one SeriesSort object. + \\"\\"\\" + input SeriesSort { + cost: SortDirection + episodes: SortDirection + title: SortDirection + } + + input SeriesUpdateInput { + cost: Float + cost_ADD: Float + cost_DIVIDE: Float + cost_MULTIPLY: Float + cost_SUBTRACT: Float + episodes: Int + episodes_DECREMENT: Int + episodes_INCREMENT: Int + title: String + } + + input SeriesWhere { + AND: [SeriesWhere!] + NOT: SeriesWhere + OR: [SeriesWhere!] + cost: Float + cost_GT: Float + cost_GTE: Float + cost_IN: [Float!] + cost_LT: Float + cost_LTE: Float + cost_NOT: Float @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + cost_NOT_IN: [Float!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + episodes: Int + episodes_GT: Int + episodes_GTE: Int + episodes_IN: [Int!] + episodes_LT: Int + episodes_LTE: Int + episodes_NOT: Int @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + episodes_NOT_IN: [Int!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title: String + title_CONTAINS: String + title_ENDS_WITH: String + title_IN: [String!] + title_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_STARTS_WITH: String + } + + \\"\\"\\"An enum for sorting in either ascending or descending order.\\"\\"\\" + enum SortDirection { + \\"\\"\\"Sort by field values in ascending order.\\"\\"\\" + ASC + \\"\\"\\"Sort by field values in descending order.\\"\\"\\" + DESC + } + + type StringAggregateSelectionNonNullable { + longest: String! + shortest: String! + } + + type UpdateActorsMutationResponse { + actors: [Actor!]! + info: UpdateInfo! + } + + \\"\\"\\" + Information about the number of nodes and relationships created and deleted during an update mutation + \\"\\"\\" + type UpdateInfo { + bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") + nodesCreated: Int! + nodesDeleted: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + type UpdateMoviesMutationResponse { + info: UpdateInfo! + movies: [Movie!]! + } + + type UpdateSeriesMutationResponse { + info: UpdateInfo! + series: [Series!]! + }" + `); + }); +}); diff --git a/packages/graphql/tests/schema/experimental-schema/issues/2377.test.ts b/packages/graphql/tests/schema/experimental-schema/issues/2377.test.ts index f58be04674a..0c81fdffe9f 100644 --- a/packages/graphql/tests/schema/experimental-schema/issues/2377.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/issues/2377.test.ts @@ -357,6 +357,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => { properties: [Property!] tags: [Tag!] type: ResourceType! + updatedAt: DateTime! } input ResourceDeleteInput { @@ -388,8 +389,8 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => { name: StringAggregateSelectionNullable! } - input ResourceEntityImplementationsWhere { - Resource: ResourceWhere + enum ResourceEntityImplementation { + Resource } input ResourceEntityOptions { @@ -411,7 +412,9 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => { } input ResourceEntityWhere { - _on: ResourceEntityImplementationsWhere + AND: [ResourceEntityWhere!] + NOT: ResourceEntityWhere + OR: [ResourceEntityWhere!] id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -444,6 +447,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => { type_IN: [ResourceType!] type_NOT: ResourceType @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") type_NOT_IN: [ResourceType!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + typename_IN: [ResourceEntityImplementation!] } input ResourceOnCreateInput { @@ -453,6 +457,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => { properties: [Property!] tags: [Tag!] type: ResourceType! + updatedAt: DateTime! } input ResourceOptions { @@ -503,6 +508,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => { input ResourceUpdateInput { containedBy: [ResourceContainedByUpdateFieldInput!] + createdAt: DateTime externalIds: [ID!] externalIds_POP: Int externalIds_PUSH: [ID!] diff --git a/packages/graphql/tests/schema/experimental-schema/issues/2993.test.ts b/packages/graphql/tests/schema/experimental-schema/issues/2993.test.ts index eee73a976bb..d05a6a1d3d1 100644 --- a/packages/graphql/tests/schema/experimental-schema/issues/2993.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/issues/2993.test.ts @@ -89,6 +89,10 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { since: SortDirection } + input FOLLOWSUpdateInput { + since: DateTime + } + input FOLLOWSWhere { AND: [FOLLOWSWhere!] NOT: FOLLOWSWhere @@ -153,6 +157,10 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { _on: ProfileImplementationsDisconnectInput } + enum ProfileImplementation { + User + } + input ProfileImplementationsConnectInput { User: [UserConnectInput!] } @@ -169,10 +177,6 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { User: UserUpdateInput } - input ProfileImplementationsWhere { - User: UserWhere - } - input ProfileOptions { limit: Int offset: Int @@ -197,7 +201,9 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { } input ProfileWhere { - _on: ProfileImplementationsWhere + AND: [ProfileWhere!] + NOT: ProfileWhere + OR: [ProfileWhere!] id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -208,6 +214,7 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { id_NOT_IN: [ID!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_STARTS_WITH: ID + typename_IN: [ProfileImplementation!] userName: String userName_CONTAINS: String userName_ENDS_WITH: String @@ -345,6 +352,7 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { } input UserFollowingUpdateConnectionInput { + edge: FOLLOWSUpdateInput node: ProfileUpdateInput } @@ -515,6 +523,10 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { since: SortDirection } + input FOLLOWSUpdateInput { + since: DateTime + } + input FOLLOWSWhere { AND: [FOLLOWSWhere!] NOT: FOLLOWSWhere @@ -579,6 +591,10 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { _on: ProfileImplementationsDisconnectInput } + enum ProfileImplementation { + User + } + input ProfileImplementationsConnectInput { User: [UserConnectInput!] } @@ -595,10 +611,6 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { User: UserUpdateInput } - input ProfileImplementationsWhere { - User: UserWhere - } - input ProfileOptions { limit: Int offset: Int @@ -622,7 +634,9 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { } input ProfileWhere { - _on: ProfileImplementationsWhere + AND: [ProfileWhere!] + NOT: ProfileWhere + OR: [ProfileWhere!] id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -633,6 +647,7 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { id_NOT_IN: [ID!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_STARTS_WITH: ID + typename_IN: [ProfileImplementation!] userName: String userName_CONTAINS: String userName_ENDS_WITH: String @@ -770,6 +785,7 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { } input UserFollowingUpdateConnectionInput { + edge: FOLLOWSUpdateInput node: ProfileUpdateInput } diff --git a/packages/graphql/tests/schema/experimental-schema/issues/3439.test.ts b/packages/graphql/tests/schema/experimental-schema/issues/3439.test.ts index fc90226a296..789f9efffad 100644 --- a/packages/graphql/tests/schema/experimental-schema/issues/3439.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/issues/3439.test.ts @@ -18,11 +18,11 @@ */ import { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { validateSchema } from "graphql"; import { gql } from "graphql-tag"; import { lexicographicSortSchema } from "graphql/utilities"; import { Neo4jGraphQL } from "../../../../src"; import { TestSubscriptionsEngine } from "../../../utils/TestSubscriptionsEngine"; -import { validateSchema } from "graphql"; describe("https://github.com/neo4j/graphql/issues/3439", () => { test("Type definitions implementing multiple interfaces", async () => { @@ -59,7 +59,11 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { `; const subscriptionsEngine = new TestSubscriptionsEngine(); - const neoSchema = new Neo4jGraphQL({ typeDefs, features: { subscriptions: subscriptionsEngine } }); + const neoSchema = new Neo4jGraphQL({ + typeDefs, + features: { subscriptions: subscriptionsEngine }, + experimental: true, + }); const schema = await neoSchema.getSchema(); const errors = validateSchema(schema); @@ -118,6 +122,7 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { type Genre { name: String! product(directed: Boolean = true, options: IProductOptions, where: IProductWhere): [IProduct!]! + productAggregate(directed: Boolean = true, where: IProductWhere): GenreIProductProductAggregationSelection productConnection(after: String, directed: Boolean = true, first: Int, sort: [GenreProductConnectionSort!], where: GenreProductConnectionWhere): GenreProductConnection! } @@ -176,6 +181,16 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { name: String! } + type GenreIProductProductAggregationSelection { + count: Int! + node: GenreIProductProductNodeAggregateSelection + } + + type GenreIProductProductNodeAggregateSelection { + id: StringAggregateSelectionNonNullable! + name: StringAggregateSelectionNonNullable! + } + input GenreOnCreateInput { name: String! } @@ -380,12 +395,61 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { id: String! } + type INodeAggregateSelection { + count: Int! + id: StringAggregateSelectionNonNullable! + } + + enum INodeImplementation { + Movie + Series + } + + input INodeOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more INodeSort objects to sort INodes by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [INodeSort] + } + + \\"\\"\\" + Fields to sort INodes by. The order in which sorts are applied is not guaranteed when specifying many fields in one INodeSort object. + \\"\\"\\" + input INodeSort { + id: SortDirection + } + + input INodeWhere { + AND: [INodeWhere!] + NOT: INodeWhere + OR: [INodeWhere!] + id: String + id_CONTAINS: String + id_ENDS_WITH: String + id_IN: [String!] + id_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + id_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + id_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + id_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + id_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + id_STARTS_WITH: String + typename_IN: [INodeImplementation!] + } + interface IProduct { genre: Genre! id: String! name: String! } + type IProductAggregateSelection { + count: Int! + id: StringAggregateSelectionNonNullable! + name: StringAggregateSelectionNonNullable! + } + input IProductConnectInput { _on: IProductImplementationsConnectInput } @@ -412,6 +476,11 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { name: String! } + enum IProductImplementation { + Movie + Series + } + input IProductImplementationsConnectInput { Movie: [MovieConnectInput!] Series: [SeriesConnectInput!] @@ -427,21 +496,11 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { Series: [SeriesDisconnectInput!] } - input IProductImplementationsSubscriptionWhere { - Movie: MovieSubscriptionWhere - Series: SeriesSubscriptionWhere - } - input IProductImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input IProductImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input IProductOptions { limit: Int offset: Int @@ -463,7 +522,6 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { AND: [IProductSubscriptionWhere!] NOT: IProductSubscriptionWhere OR: [IProductSubscriptionWhere!] - _on: IProductImplementationsSubscriptionWhere id: String id_CONTAINS: String id_ENDS_WITH: String @@ -493,7 +551,9 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { } input IProductWhere { - _on: IProductImplementationsWhere + AND: [IProductWhere!] + NOT: IProductWhere + OR: [IProductWhere!] id: String id_CONTAINS: String id_ENDS_WITH: String @@ -514,6 +574,7 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { name_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [IProductImplementation!] } type Movie implements INode & IProduct { @@ -880,6 +941,10 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { genres(options: GenreOptions, where: GenreWhere): [Genre!]! genresAggregate(where: GenreWhere): GenreAggregateSelection! genresConnection(after: String, first: Int, sort: [GenreSort], where: GenreWhere): GenresConnection! + iNodes(options: INodeOptions, where: INodeWhere): [INode!]! + iNodesAggregate(where: INodeWhere): INodeAggregateSelection! + iProducts(options: IProductOptions, where: IProductWhere): [IProduct!]! + iProductsAggregate(where: IProductWhere): IProductAggregateSelection! movies(options: MovieOptions, where: MovieWhere): [Movie!]! moviesAggregate(where: MovieWhere): MovieAggregateSelection! moviesConnection(after: String, first: Int, sort: [MovieSort], where: MovieWhere): MoviesConnection! @@ -1317,7 +1382,11 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { `; const subscriptionsEngine = new TestSubscriptionsEngine(); - const neoSchema = new Neo4jGraphQL({ typeDefs, features: { subscriptions: subscriptionsEngine } }); + const neoSchema = new Neo4jGraphQL({ + typeDefs, + features: { subscriptions: subscriptionsEngine }, + experimental: true, + }); const schema = await neoSchema.getSchema(); const errors = validateSchema(schema); @@ -1376,6 +1445,7 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { type Genre { name: String! product(directed: Boolean = true, options: IProductOptions, where: IProductWhere): [IProduct!]! + productAggregate(directed: Boolean = true, where: IProductWhere): GenreIProductProductAggregationSelection productConnection(after: String, directed: Boolean = true, first: Int, sort: [GenreProductConnectionSort!], where: GenreProductConnectionWhere): GenreProductConnection! } @@ -1434,6 +1504,16 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { name: String! } + type GenreIProductProductAggregationSelection { + count: Int! + node: GenreIProductProductNodeAggregateSelection + } + + type GenreIProductProductNodeAggregateSelection { + id: StringAggregateSelectionNonNullable! + name: StringAggregateSelectionNonNullable! + } + input GenreOnCreateInput { name: String! } @@ -1640,6 +1720,12 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { name: String! } + type IProductAggregateSelection { + count: Int! + id: StringAggregateSelectionNonNullable! + name: StringAggregateSelectionNonNullable! + } + input IProductConnectInput { _on: IProductImplementationsConnectInput } @@ -1741,6 +1827,11 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { where: IProductGenreConnectionWhere } + enum IProductImplementation { + Movie + Series + } + input IProductImplementationsConnectInput { Movie: [MovieConnectInput!] Series: [SeriesConnectInput!] @@ -1756,21 +1847,11 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { Series: [SeriesDisconnectInput!] } - input IProductImplementationsSubscriptionWhere { - Movie: MovieSubscriptionWhere - Series: SeriesSubscriptionWhere - } - input IProductImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input IProductImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input IProductOptions { limit: Int offset: Int @@ -1792,7 +1873,6 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { AND: [IProductSubscriptionWhere!] NOT: IProductSubscriptionWhere OR: [IProductSubscriptionWhere!] - _on: IProductImplementationsSubscriptionWhere id: String id_CONTAINS: String id_ENDS_WITH: String @@ -1822,7 +1902,9 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { } input IProductWhere { - _on: IProductImplementationsWhere + AND: [IProductWhere!] + NOT: IProductWhere + OR: [IProductWhere!] id: String id_CONTAINS: String id_ENDS_WITH: String @@ -1843,6 +1925,7 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { name_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [IProductImplementation!] } type Movie implements IProduct { @@ -2134,6 +2217,8 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { genres(options: GenreOptions, where: GenreWhere): [Genre!]! genresAggregate(where: GenreWhere): GenreAggregateSelection! genresConnection(after: String, first: Int, sort: [GenreSort], where: GenreWhere): GenresConnection! + iProducts(options: IProductOptions, where: IProductWhere): [IProduct!]! + iProductsAggregate(where: IProductWhere): IProductAggregateSelection! movies(options: MovieOptions, where: MovieWhere): [Movie!]! moviesAggregate(where: MovieWhere): MovieAggregateSelection! moviesConnection(after: String, first: Int, sort: [MovieSort], where: MovieWhere): MoviesConnection! @@ -2496,7 +2581,11 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { `; const subscriptionsEngine = new TestSubscriptionsEngine(); - const neoSchema = new Neo4jGraphQL({ typeDefs, features: { subscriptions: subscriptionsEngine } }); + const neoSchema = new Neo4jGraphQL({ + typeDefs, + features: { subscriptions: subscriptionsEngine }, + experimental: true, + }); const schema = await neoSchema.getSchema(); const errors = validateSchema(schema); @@ -2555,6 +2644,7 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { type Genre { name: String! product(directed: Boolean = true, options: IProductOptions, where: IProductWhere): [IProduct!]! + productAggregate(directed: Boolean = true, where: IProductWhere): GenreIProductProductAggregationSelection productConnection(after: String, directed: Boolean = true, first: Int, sort: [GenreProductConnectionSort!], where: GenreProductConnectionWhere): GenreProductConnection! } @@ -2605,6 +2695,16 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { name: String! } + type GenreIProductProductAggregationSelection { + count: Int! + node: GenreIProductProductNodeAggregateSelection + } + + type GenreIProductProductNodeAggregateSelection { + id: StringAggregateSelectionNonNullable! + name: StringAggregateSelectionNonNullable! + } + input GenreOptions { limit: Int offset: Int @@ -2800,6 +2900,12 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { name: String! } + type IProductAggregateSelection { + count: Int! + id: StringAggregateSelectionNonNullable! + name: StringAggregateSelectionNonNullable! + } + input IProductConnectWhere { node: IProductWhere! } @@ -2814,9 +2920,9 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { name: String! } - input IProductImplementationsSubscriptionWhere { - Movie: MovieSubscriptionWhere - Series: SeriesSubscriptionWhere + enum IProductImplementation { + Movie + Series } input IProductImplementationsUpdateInput { @@ -2824,11 +2930,6 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { Series: SeriesUpdateInput } - input IProductImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input IProductOptions { limit: Int offset: Int @@ -2850,7 +2951,6 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { AND: [IProductSubscriptionWhere!] NOT: IProductSubscriptionWhere OR: [IProductSubscriptionWhere!] - _on: IProductImplementationsSubscriptionWhere id: String id_CONTAINS: String id_ENDS_WITH: String @@ -2880,7 +2980,9 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { } input IProductWhere { - _on: IProductImplementationsWhere + AND: [IProductWhere!] + NOT: IProductWhere + OR: [IProductWhere!] id: String id_CONTAINS: String id_ENDS_WITH: String @@ -2901,6 +3003,7 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { name_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") name_STARTS_WITH: String + typename_IN: [IProductImplementation!] } type Movie implements IProduct { @@ -3053,6 +3156,8 @@ describe("https://github.com/neo4j/graphql/issues/3439", () => { genres(options: GenreOptions, where: GenreWhere): [Genre!]! genresAggregate(where: GenreWhere): GenreAggregateSelection! genresConnection(after: String, first: Int, sort: [GenreSort], where: GenreWhere): GenresConnection! + iProducts(options: IProductOptions, where: IProductWhere): [IProduct!]! + iProductsAggregate(where: IProductWhere): IProductAggregateSelection! movies(options: MovieOptions, where: MovieWhere): [Movie!]! moviesAggregate(where: MovieWhere): MovieAggregateSelection! moviesConnection(after: String, first: Int, sort: [MovieSort], where: MovieWhere): MoviesConnection! diff --git a/packages/graphql/tests/schema/experimental-schema/math.test.ts b/packages/graphql/tests/schema/experimental-schema/math.test.ts index 6b41ae86e07..364fd3cda3d 100644 --- a/packages/graphql/tests/schema/experimental-schema/math.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/math.test.ts @@ -1664,6 +1664,10 @@ describe("Algebraic", () => { _on: ProductionImplementationsDisconnectInput } + enum ProductionImplementation { + Movie + } + input ProductionImplementationsConnectInput { Movie: [MovieConnectInput!] } @@ -1680,10 +1684,6 @@ describe("Algebraic", () => { Movie: MovieUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - } - input ProductionOptions { limit: Int offset: Int @@ -1708,7 +1708,10 @@ describe("Algebraic", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] + typename_IN: [ProductionImplementation!] viewers: Int viewers_GT: Int viewers_GTE: Int diff --git a/packages/graphql/tests/schema/experimental-schema/nested-aggregation-on-interface.test.ts b/packages/graphql/tests/schema/experimental-schema/nested-aggregation-on-interface.test.ts index 544299be2c8..c53e8d3bf88 100644 --- a/packages/graphql/tests/schema/experimental-schema/nested-aggregation-on-interface.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/nested-aggregation-on-interface.test.ts @@ -430,16 +430,16 @@ describe("nested aggregation on interface", () => { Series: SeriesCreateInput } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -468,7 +468,9 @@ describe("nested aggregation on interface", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] cost: Float cost_GT: Float cost_GTE: Float @@ -487,6 +489,7 @@ describe("nested aggregation on interface", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { @@ -1047,16 +1050,16 @@ describe("nested aggregation on interface", () => { Series: SeriesCreateInput } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -1085,7 +1088,9 @@ describe("nested aggregation on interface", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] cost: Float cost_GT: Float cost_GTE: Float @@ -1104,6 +1109,7 @@ describe("nested aggregation on interface", () => { title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") title_STARTS_WITH: String + typename_IN: [ProductionImplementation!] } type Query { diff --git a/packages/graphql/tests/schema/experimental-schema/plural.test.ts b/packages/graphql/tests/schema/experimental-schema/plural.test.ts new file mode 100644 index 00000000000..c616a4401f8 --- /dev/null +++ b/packages/graphql/tests/schema/experimental-schema/plural.test.ts @@ -0,0 +1,338 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { gql } from "graphql-tag"; +import { lexicographicSortSchema } from "graphql/utilities"; +import { Neo4jGraphQL } from "../../../src"; + +describe("Experimental Plural option", () => { + test("Plural on interface and union", async () => { + const typeDefs = gql` + interface Animal @plural(value: "animales") { + name: String + } + + type Dog implements Animal { + name: String + breed: String + } + + type Cat { + queenOf: String + } + + union Pet @plural(value: "petties") = Dog | Cat + `; + const neoSchema = new Neo4jGraphQL({ typeDefs, experimental: true }); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + interface Animal { + name: String + } + + type AnimalAggregateSelection { + count: Int! + name: StringAggregateSelectionNullable! + } + + enum AnimalImplementation { + Dog + } + + input AnimalOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more AnimalSort objects to sort Animales by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [AnimalSort] + } + + \\"\\"\\" + Fields to sort Animales by. The order in which sorts are applied is not guaranteed when specifying many fields in one AnimalSort object. + \\"\\"\\" + input AnimalSort { + name: SortDirection + } + + input AnimalWhere { + AND: [AnimalWhere!] + NOT: AnimalWhere + OR: [AnimalWhere!] + name: String + name_CONTAINS: String + name_ENDS_WITH: String + name_IN: [String] + name_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_STARTS_WITH: String + typename_IN: [AnimalImplementation!] + } + + type Cat { + queenOf: String + } + + type CatAggregateSelection { + count: Int! + queenOf: StringAggregateSelectionNullable! + } + + input CatCreateInput { + queenOf: String + } + + type CatEdge { + cursor: String! + node: Cat! + } + + input CatOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more CatSort objects to sort Cats by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [CatSort!] + } + + \\"\\"\\" + Fields to sort Cats by. The order in which sorts are applied is not guaranteed when specifying many fields in one CatSort object. + \\"\\"\\" + input CatSort { + queenOf: SortDirection + } + + input CatUpdateInput { + queenOf: String + } + + input CatWhere { + AND: [CatWhere!] + NOT: CatWhere + OR: [CatWhere!] + queenOf: String + queenOf_CONTAINS: String + queenOf_ENDS_WITH: String + queenOf_IN: [String] + queenOf_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + queenOf_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + queenOf_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + queenOf_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + queenOf_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + queenOf_STARTS_WITH: String + } + + type CatsConnection { + edges: [CatEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + type CreateCatsMutationResponse { + cats: [Cat!]! + info: CreateInfo! + } + + type CreateDogsMutationResponse { + dogs: [Dog!]! + info: CreateInfo! + } + + \\"\\"\\" + Information about the number of nodes and relationships created during a create mutation + \\"\\"\\" + type CreateInfo { + bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") + nodesCreated: Int! + relationshipsCreated: Int! + } + + \\"\\"\\" + Information about the number of nodes and relationships deleted during a delete mutation + \\"\\"\\" + type DeleteInfo { + bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type Dog implements Animal { + breed: String + name: String + } + + type DogAggregateSelection { + breed: StringAggregateSelectionNullable! + count: Int! + name: StringAggregateSelectionNullable! + } + + input DogCreateInput { + breed: String + name: String + } + + type DogEdge { + cursor: String! + node: Dog! + } + + input DogOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more DogSort objects to sort Dogs by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [DogSort!] + } + + \\"\\"\\" + Fields to sort Dogs by. The order in which sorts are applied is not guaranteed when specifying many fields in one DogSort object. + \\"\\"\\" + input DogSort { + breed: SortDirection + name: SortDirection + } + + input DogUpdateInput { + breed: String + name: String + } + + input DogWhere { + AND: [DogWhere!] + NOT: DogWhere + OR: [DogWhere!] + breed: String + breed_CONTAINS: String + breed_ENDS_WITH: String + breed_IN: [String] + breed_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + breed_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + breed_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + breed_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + breed_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + breed_STARTS_WITH: String + name: String + name_CONTAINS: String + name_ENDS_WITH: String + name_IN: [String] + name_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_STARTS_WITH: String + } + + type DogsConnection { + edges: [DogEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + type Mutation { + createCats(input: [CatCreateInput!]!): CreateCatsMutationResponse! + createDogs(input: [DogCreateInput!]!): CreateDogsMutationResponse! + deleteCats(where: CatWhere): DeleteInfo! + deleteDogs(where: DogWhere): DeleteInfo! + updateCats(update: CatUpdateInput, where: CatWhere): UpdateCatsMutationResponse! + updateDogs(update: DogUpdateInput, where: DogWhere): UpdateDogsMutationResponse! + } + + \\"\\"\\"Pagination information (Relay)\\"\\"\\" + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + union Pet = Cat | Dog + + input PetWhere { + Cat: CatWhere + Dog: DogWhere + } + + type Query { + animales(options: AnimalOptions, where: AnimalWhere): [Animal!]! + animalesAggregate(where: AnimalWhere): AnimalAggregateSelection! + cats(options: CatOptions, where: CatWhere): [Cat!]! + catsAggregate(where: CatWhere): CatAggregateSelection! + catsConnection(after: String, first: Int, sort: [CatSort], where: CatWhere): CatsConnection! + dogs(options: DogOptions, where: DogWhere): [Dog!]! + dogsAggregate(where: DogWhere): DogAggregateSelection! + dogsConnection(after: String, first: Int, sort: [DogSort], where: DogWhere): DogsConnection! + petties(options: QueryOptions, where: PetWhere): [Pet!]! + } + + \\"\\"\\"Input type for options that can be specified on a query operation.\\"\\"\\" + input QueryOptions { + limit: Int + offset: Int + } + + \\"\\"\\"An enum for sorting in either ascending or descending order.\\"\\"\\" + enum SortDirection { + \\"\\"\\"Sort by field values in ascending order.\\"\\"\\" + ASC + \\"\\"\\"Sort by field values in descending order.\\"\\"\\" + DESC + } + + type StringAggregateSelectionNullable { + longest: String + shortest: String + } + + type UpdateCatsMutationResponse { + cats: [Cat!]! + info: UpdateInfo! + } + + type UpdateDogsMutationResponse { + dogs: [Dog!]! + info: UpdateInfo! + } + + \\"\\"\\" + Information about the number of nodes and relationships created and deleted during an update mutation + \\"\\"\\" + type UpdateInfo { + bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") + nodesCreated: Int! + nodesDeleted: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + }" + `); + }); +}); diff --git a/packages/graphql/tests/schema/experimental-schema/subscriptions.test.ts b/packages/graphql/tests/schema/experimental-schema/subscriptions.test.ts index 5c17f1630d5..2f5539df586 100644 --- a/packages/graphql/tests/schema/experimental-schema/subscriptions.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/subscriptions.test.ts @@ -18,8 +18,8 @@ */ import { printSchemaWithDirectives } from "@graphql-tools/utils"; -import { lexicographicSortSchema } from "graphql/utilities"; import { gql } from "graphql-tag"; +import { lexicographicSortSchema } from "graphql/utilities"; import { Neo4jGraphQL } from "../../../src"; import { TestSubscriptionsEngine } from "../../utils/TestSubscriptionsEngine"; @@ -49,6 +49,7 @@ describe("Subscriptions", () => { features: { subscriptions: plugin, }, + experimental: true, }); const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); @@ -689,6 +690,7 @@ describe("Subscriptions", () => { features: { subscriptions: plugin, }, + experimental: true, }); const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); @@ -1455,6 +1457,7 @@ describe("Subscriptions", () => { features: { subscriptions: plugin, }, + experimental: true, }); const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); @@ -1851,6 +1854,7 @@ describe("Subscriptions", () => { actorCount_LTE: Int actorCount_NOT: Int @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") actorCount_NOT_IN: [Int] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + actors: ActorWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -1869,6 +1873,15 @@ describe("Subscriptions", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related Actors match this filter\\"\\"\\" + actors_ALL: ActorWhere + \\"\\"\\"Return Movies where none of the related Actors match this filter\\"\\"\\" + actors_NONE: ActorWhere + actors_NOT: ActorWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related Actors match this filter\\"\\"\\" + actors_SINGLE: ActorWhere + \\"\\"\\"Return Movies where some of the related Actors match this filter\\"\\"\\" + actors_SOME: ActorWhere averageRating: Float averageRating_GT: Float averageRating_GTE: Float @@ -2208,6 +2221,7 @@ describe("Subscriptions", () => { } type Query { + actors(options: QueryOptions, where: ActorWhere): [Actor!]! movies(options: MovieOptions, where: MovieWhere): [Movie!]! moviesAggregate(where: MovieWhere): MovieAggregateSelection! moviesConnection(after: String, first: Int, sort: [MovieSort], where: MovieWhere): MoviesConnection! @@ -2593,6 +2607,7 @@ describe("Subscriptions", () => { features: { subscriptions: plugin, }, + experimental: true, }); const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); @@ -3473,6 +3488,7 @@ describe("Subscriptions", () => { features: { subscriptions: plugin, }, + experimental: true, }); const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); @@ -3997,6 +4013,7 @@ describe("Subscriptions", () => { features: { subscriptions: plugin, }, + experimental: true, }); const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); @@ -4603,6 +4620,7 @@ describe("Subscriptions", () => { features: { subscriptions: plugin, }, + experimental: true, }); const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); @@ -4999,6 +5017,7 @@ describe("Subscriptions", () => { actorCount_LTE: Int actorCount_NOT: Int @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") actorCount_NOT_IN: [Int] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + actors: ActorWhere @deprecated(reason: \\"Use \`actors_SOME\` instead.\\") actorsConnection: MovieActorsConnectionWhere @deprecated(reason: \\"Use \`actorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieActorsConnections match this filter @@ -5017,6 +5036,15 @@ describe("Subscriptions", () => { Return Movies where some of the related MovieActorsConnections match this filter \\"\\"\\" actorsConnection_SOME: MovieActorsConnectionWhere + \\"\\"\\"Return Movies where all of the related Actors match this filter\\"\\"\\" + actors_ALL: ActorWhere + \\"\\"\\"Return Movies where none of the related Actors match this filter\\"\\"\\" + actors_NONE: ActorWhere + actors_NOT: ActorWhere @deprecated(reason: \\"Use \`actors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related Actors match this filter\\"\\"\\" + actors_SINGLE: ActorWhere + \\"\\"\\"Return Movies where some of the related Actors match this filter\\"\\"\\" + actors_SOME: ActorWhere averageRating: Float averageRating_GT: Float averageRating_GTE: Float @@ -5356,6 +5384,7 @@ describe("Subscriptions", () => { } type Query { + actors(options: QueryOptions, where: ActorWhere): [Actor!]! movies(options: MovieOptions, where: MovieWhere): [Movie!]! moviesAggregate(where: MovieWhere): MovieAggregateSelection! moviesConnection(after: String, first: Int, sort: [MovieSort], where: MovieWhere): MoviesConnection! @@ -5689,6 +5718,7 @@ describe("Subscriptions", () => { features: { subscriptions: plugin, }, + experimental: true, }); const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); @@ -5729,6 +5759,10 @@ describe("Subscriptions", () => { moviesConnection(after: String, directed: Boolean = true, first: Int, sort: [CreatureMoviesConnectionSort!], where: CreatureMoviesConnectionWhere): CreatureMoviesConnection! } + type CreatureAggregateSelection { + count: Int! + } + input CreatureConnectInput { _on: CreatureImplementationsConnectInput movies: CreatureMoviesConnectFieldInput @@ -5752,6 +5786,10 @@ describe("Subscriptions", () => { movies: CreatureMoviesDisconnectFieldInput } + enum CreatureImplementation { + Person + } + input CreatureImplementationsConnectInput { Person: [PersonConnectInput!] } @@ -5768,10 +5806,6 @@ describe("Subscriptions", () => { Person: PersonUpdateInput } - input CreatureImplementationsWhere { - Person: PersonWhere - } - input CreatureMoviesConnectFieldInput { connect: ProductionConnectInput where: ProductionConnectWhere @@ -5843,9 +5877,12 @@ describe("Subscriptions", () => { } input CreatureWhere { - _on: CreatureImplementationsWhere + AND: [CreatureWhere!] + NOT: CreatureWhere + OR: [CreatureWhere!] moviesConnection: CreatureMoviesConnectionWhere moviesConnection_NOT: CreatureMoviesConnectionWhere + typename_IN: [CreatureImplementation!] } \\"\\"\\" @@ -5879,6 +5916,7 @@ describe("Subscriptions", () => { type Movie implements Production { director(directed: Boolean = true, options: CreatureOptions, where: CreatureWhere): Creature! + directorAggregate(directed: Boolean = true, where: CreatureWhere): MovieCreatureDirectorAggregationSelection directorConnection(after: String, directed: Boolean = true, first: Int, where: ProductionDirectorConnectionWhere): ProductionDirectorConnection! id: ID title: String! @@ -5900,6 +5938,10 @@ describe("Subscriptions", () => { title: String! } + type MovieCreatureDirectorAggregationSelection { + count: Int! + } + input MovieDeleteInput { director: MovieDirectorDeleteFieldInput } @@ -5966,32 +6008,6 @@ describe("Subscriptions", () => { title: SortDirection } - input MovieSubscriptionWhere { - AND: [MovieSubscriptionWhere!] - NOT: MovieSubscriptionWhere - OR: [MovieSubscriptionWhere!] - id: ID - id_CONTAINS: ID - id_ENDS_WITH: ID - id_IN: [ID] - id_NOT: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - id_NOT_CONTAINS: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - id_NOT_ENDS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - id_NOT_IN: [ID] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - id_STARTS_WITH: ID - title: String - title_CONTAINS: String - title_ENDS_WITH: String - title_IN: [String] - title_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - title_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - title_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - title_NOT_IN: [String] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") - title_STARTS_WITH: String - } - input MovieUpdateInput { director: MovieDirectorUpdateFieldInput id: ID @@ -6060,6 +6076,7 @@ describe("Subscriptions", () => { type Person implements Creature { movies(directed: Boolean = true, options: ProductionOptions, where: ProductionWhere): Production! + moviesAggregate(directed: Boolean = true, where: ProductionWhere): PersonProductionMoviesAggregationSelection moviesConnection(after: String, directed: Boolean = true, first: Int, sort: [CreatureMoviesConnectionSort!], where: CreatureMoviesConnectionWhere): CreatureMoviesConnection! } @@ -6147,6 +6164,15 @@ describe("Subscriptions", () => { offset: Int } + type PersonProductionMoviesAggregationSelection { + count: Int! + node: PersonProductionMoviesNodeAggregateSelection + } + + type PersonProductionMoviesNodeAggregateSelection { + id: IDAggregateSelectionNullable! + } + input PersonRelationInput { movies: PersonMoviesCreateFieldInput } @@ -6204,6 +6230,11 @@ describe("Subscriptions", () => { id: ID } + type ProductionAggregateSelection { + count: Int! + id: IDAggregateSelectionNullable! + } + input ProductionConnectInput { _on: ProductionImplementationsConnectInput director: ProductionDirectorConnectFieldInput @@ -6288,6 +6319,11 @@ describe("Subscriptions", () => { id: ID } + enum ProductionImplementation { + Movie + Series + } + input ProductionImplementationsConnectInput { Movie: [MovieConnectInput!] Series: [SeriesConnectInput!] @@ -6303,21 +6339,11 @@ describe("Subscriptions", () => { Series: [SeriesDisconnectInput!] } - input ProductionImplementationsSubscriptionWhere { - Movie: MovieSubscriptionWhere - Series: SeriesSubscriptionWhere - } - input ProductionImplementationsUpdateInput { Movie: MovieUpdateInput Series: SeriesUpdateInput } - input ProductionImplementationsWhere { - Movie: MovieWhere - Series: SeriesWhere - } - input ProductionOptions { limit: Int offset: Int @@ -6338,7 +6364,6 @@ describe("Subscriptions", () => { AND: [ProductionSubscriptionWhere!] NOT: ProductionSubscriptionWhere OR: [ProductionSubscriptionWhere!] - _on: ProductionImplementationsSubscriptionWhere id: ID id_CONTAINS: ID id_ENDS_WITH: ID @@ -6358,7 +6383,9 @@ describe("Subscriptions", () => { } input ProductionWhere { - _on: ProductionImplementationsWhere + AND: [ProductionWhere!] + NOT: ProductionWhere + OR: [ProductionWhere!] directorConnection: ProductionDirectorConnectionWhere directorConnection_NOT: ProductionDirectorConnectionWhere id: ID @@ -6371,15 +6398,20 @@ describe("Subscriptions", () => { id_NOT_IN: [ID] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_NOT_STARTS_WITH: ID @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") id_STARTS_WITH: ID + typename_IN: [ProductionImplementation!] } type Query { + creatures(options: CreatureOptions, where: CreatureWhere): [Creature!]! + creaturesAggregate(where: CreatureWhere): CreatureAggregateSelection! movies(options: MovieOptions, where: MovieWhere): [Movie!]! moviesAggregate(where: MovieWhere): MovieAggregateSelection! moviesConnection(after: String, first: Int, sort: [MovieSort], where: MovieWhere): MoviesConnection! people(options: PersonOptions, where: PersonWhere): [Person!]! peopleAggregate(where: PersonWhere): PersonAggregateSelection! peopleConnection(after: String, first: Int, where: PersonWhere): PeopleConnection! + productions(options: ProductionOptions, where: ProductionWhere): [Production!]! + productionsAggregate(where: ProductionWhere): ProductionAggregateSelection! series(options: SeriesOptions, where: SeriesWhere): [Series!]! seriesAggregate(where: SeriesWhere): SeriesAggregateSelection! seriesConnection(after: String, first: Int, sort: [SeriesSort], where: SeriesWhere): SeriesConnection! @@ -6387,6 +6419,7 @@ describe("Subscriptions", () => { type Series implements Production { director(directed: Boolean = true, options: CreatureOptions, where: CreatureWhere): Creature! + directorAggregate(directed: Boolean = true, where: CreatureWhere): SeriesCreatureDirectorAggregationSelection directorConnection(after: String, directed: Boolean = true, first: Int, where: ProductionDirectorConnectionWhere): ProductionDirectorConnection! episode: Int! id: ID @@ -6423,6 +6456,10 @@ describe("Subscriptions", () => { timestamp: Float! } + type SeriesCreatureDirectorAggregationSelection { + count: Int! + } + input SeriesDeleteInput { director: SeriesDirectorDeleteFieldInput } diff --git a/packages/graphql/tests/schema/experimental-schema/union-interface-relationship.test.ts b/packages/graphql/tests/schema/experimental-schema/union-interface-relationship.test.ts index d3b19ec768d..bc5eb4f0465 100644 --- a/packages/graphql/tests/schema/experimental-schema/union-interface-relationship.test.ts +++ b/packages/graphql/tests/schema/experimental-schema/union-interface-relationship.test.ts @@ -1255,6 +1255,7 @@ describe("Union Interface Relationships", () => { actors_SINGLE: ActorWhere \\"\\"\\"Return Movies where some of the related Actors match this filter\\"\\"\\" actors_SOME: ActorWhere + directors: DirectorWhere @deprecated(reason: \\"Use \`directors_SOME\` instead.\\") directorsConnection: MovieDirectorsConnectionWhere @deprecated(reason: \\"Use \`directorsConnection_SOME\` instead.\\") \\"\\"\\" Return Movies where all of the related MovieDirectorsConnections match this filter @@ -1273,6 +1274,15 @@ describe("Union Interface Relationships", () => { Return Movies where some of the related MovieDirectorsConnections match this filter \\"\\"\\" directorsConnection_SOME: MovieDirectorsConnectionWhere + \\"\\"\\"Return Movies where all of the related Directors match this filter\\"\\"\\" + directors_ALL: DirectorWhere + \\"\\"\\"Return Movies where none of the related Directors match this filter\\"\\"\\" + directors_NONE: DirectorWhere + directors_NOT: DirectorWhere @deprecated(reason: \\"Use \`directors_NONE\` instead.\\") + \\"\\"\\"Return Movies where one of the related Directors match this filter\\"\\"\\" + directors_SINGLE: DirectorWhere + \\"\\"\\"Return Movies where some of the related Directors match this filter\\"\\"\\" + directors_SOME: DirectorWhere imdbId: Int imdbId_GT: Int imdbId_GTE: Int @@ -1815,6 +1825,11 @@ describe("Union Interface Relationships", () => { _on: ReviewerImplementationsDisconnectInput } + enum ReviewerImplementation { + Influencer + Person + } + input ReviewerImplementationsConnectInput { Person: [PersonConnectInput!] } @@ -1832,11 +1847,6 @@ describe("Union Interface Relationships", () => { Person: PersonUpdateInput } - input ReviewerImplementationsWhere { - Influencer: InfluencerWhere - Person: PersonWhere - } - input ReviewerOptions { limit: Int offset: Int @@ -1865,7 +1875,9 @@ describe("Union Interface Relationships", () => { } input ReviewerWhere { - _on: ReviewerImplementationsWhere + AND: [ReviewerWhere!] + NOT: ReviewerWhere + OR: [ReviewerWhere!] reputation: Int reputation_GT: Int reputation_GTE: Int @@ -1882,6 +1894,7 @@ describe("Union Interface Relationships", () => { reviewerId_LTE: Int reviewerId_NOT: Int @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") reviewerId_NOT_IN: [Int] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + typename_IN: [ReviewerImplementation!] } \\"\\"\\"An enum for sorting in either ascending or descending order.\\"\\"\\" diff --git a/packages/graphql/tests/schema/experimental-schema/union-relationship-filtering.test.ts b/packages/graphql/tests/schema/experimental-schema/union-relationship-filtering.test.ts new file mode 100644 index 00000000000..edb38bc4eda --- /dev/null +++ b/packages/graphql/tests/schema/experimental-schema/union-relationship-filtering.test.ts @@ -0,0 +1,675 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { printSchemaWithDirectives } from "@graphql-tools/utils"; +import { lexicographicSortSchema } from "graphql"; +import { Neo4jGraphQL } from "../../../src"; + +describe("Union Relationships Filtering", () => { + test("Union Relationships Filtering", async () => { + const typeDefs = ` + type Movie { + title: String! + isan: String! @unique + } + type Series { + title: String! + isan: String! @unique + } + union Production = Movie | Series + interface ActedIn @relationshipProperties { + screenTime: Int! + } + type Actor { + name: String! + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs, experimental: true }); + const printedSchema = printSchemaWithDirectives(lexicographicSortSchema(await neoSchema.getSchema())); + + expect(printedSchema).toMatchInlineSnapshot(` + "schema { + query: Query + mutation: Mutation + } + + interface ActedIn { + screenTime: Int! + } + + input ActedInCreateInput { + screenTime: Int! + } + + input ActedInSort { + screenTime: SortDirection + } + + input ActedInUpdateInput { + screenTime: Int + screenTime_DECREMENT: Int + screenTime_INCREMENT: Int + } + + input ActedInWhere { + AND: [ActedInWhere!] + NOT: ActedInWhere + OR: [ActedInWhere!] + screenTime: Int + screenTime_GT: Int + screenTime_GTE: Int + screenTime_IN: [Int!] + screenTime_LT: Int + screenTime_LTE: Int + screenTime_NOT: Int @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + screenTime_NOT_IN: [Int!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + } + + type Actor { + actedIn(directed: Boolean = true, options: QueryOptions, where: ProductionWhere): [Production!]! + actedInConnection(after: String, directed: Boolean = true, first: Int, sort: [ActorActedInConnectionSort!], where: ActorActedInConnectionWhere): ActorActedInConnection! + name: String! + } + + input ActorActedInConnectInput { + Movie: [ActorActedInMovieConnectFieldInput!] + Series: [ActorActedInSeriesConnectFieldInput!] + } + + input ActorActedInConnectOrCreateInput { + Movie: [ActorActedInMovieConnectOrCreateFieldInput!] + Series: [ActorActedInSeriesConnectOrCreateFieldInput!] + } + + type ActorActedInConnection { + edges: [ActorActedInRelationship!]! + pageInfo: PageInfo! + totalCount: Int! + } + + input ActorActedInConnectionSort { + edge: ActedInSort + } + + input ActorActedInConnectionWhere { + Movie: ActorActedInMovieConnectionWhere + Series: ActorActedInSeriesConnectionWhere + } + + input ActorActedInCreateFieldInput { + Movie: [ActorActedInMovieCreateFieldInput!] + Series: [ActorActedInSeriesCreateFieldInput!] + } + + input ActorActedInCreateInput { + Movie: ActorActedInMovieFieldInput + Series: ActorActedInSeriesFieldInput + } + + input ActorActedInDeleteInput { + Movie: [ActorActedInMovieDeleteFieldInput!] + Series: [ActorActedInSeriesDeleteFieldInput!] + } + + input ActorActedInDisconnectInput { + Movie: [ActorActedInMovieDisconnectFieldInput!] + Series: [ActorActedInSeriesDisconnectFieldInput!] + } + + input ActorActedInMovieConnectFieldInput { + edge: ActedInCreateInput! + where: MovieConnectWhere + } + + input ActorActedInMovieConnectOrCreateFieldInput { + onCreate: ActorActedInMovieConnectOrCreateFieldInputOnCreate! + where: MovieConnectOrCreateWhere! + } + + input ActorActedInMovieConnectOrCreateFieldInputOnCreate { + edge: ActedInCreateInput! + node: MovieOnCreateInput! + } + + input ActorActedInMovieConnectionWhere { + AND: [ActorActedInMovieConnectionWhere!] + NOT: ActorActedInMovieConnectionWhere + OR: [ActorActedInMovieConnectionWhere!] + edge: ActedInWhere + edge_NOT: ActedInWhere @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + node: MovieWhere + node_NOT: MovieWhere @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + } + + input ActorActedInMovieCreateFieldInput { + edge: ActedInCreateInput! + node: MovieCreateInput! + } + + input ActorActedInMovieDeleteFieldInput { + where: ActorActedInMovieConnectionWhere + } + + input ActorActedInMovieDisconnectFieldInput { + where: ActorActedInMovieConnectionWhere + } + + input ActorActedInMovieFieldInput { + connect: [ActorActedInMovieConnectFieldInput!] + connectOrCreate: [ActorActedInMovieConnectOrCreateFieldInput!] + create: [ActorActedInMovieCreateFieldInput!] + } + + input ActorActedInMovieUpdateConnectionInput { + edge: ActedInUpdateInput + node: MovieUpdateInput + } + + input ActorActedInMovieUpdateFieldInput { + connect: [ActorActedInMovieConnectFieldInput!] + connectOrCreate: [ActorActedInMovieConnectOrCreateFieldInput!] + create: [ActorActedInMovieCreateFieldInput!] + delete: [ActorActedInMovieDeleteFieldInput!] + disconnect: [ActorActedInMovieDisconnectFieldInput!] + update: ActorActedInMovieUpdateConnectionInput + where: ActorActedInMovieConnectionWhere + } + + type ActorActedInRelationship implements ActedIn { + cursor: String! + node: Production! + screenTime: Int! + } + + input ActorActedInSeriesConnectFieldInput { + edge: ActedInCreateInput! + where: SeriesConnectWhere + } + + input ActorActedInSeriesConnectOrCreateFieldInput { + onCreate: ActorActedInSeriesConnectOrCreateFieldInputOnCreate! + where: SeriesConnectOrCreateWhere! + } + + input ActorActedInSeriesConnectOrCreateFieldInputOnCreate { + edge: ActedInCreateInput! + node: SeriesOnCreateInput! + } + + input ActorActedInSeriesConnectionWhere { + AND: [ActorActedInSeriesConnectionWhere!] + NOT: ActorActedInSeriesConnectionWhere + OR: [ActorActedInSeriesConnectionWhere!] + edge: ActedInWhere + edge_NOT: ActedInWhere @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + node: SeriesWhere + node_NOT: SeriesWhere @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + } + + input ActorActedInSeriesCreateFieldInput { + edge: ActedInCreateInput! + node: SeriesCreateInput! + } + + input ActorActedInSeriesDeleteFieldInput { + where: ActorActedInSeriesConnectionWhere + } + + input ActorActedInSeriesDisconnectFieldInput { + where: ActorActedInSeriesConnectionWhere + } + + input ActorActedInSeriesFieldInput { + connect: [ActorActedInSeriesConnectFieldInput!] + connectOrCreate: [ActorActedInSeriesConnectOrCreateFieldInput!] + create: [ActorActedInSeriesCreateFieldInput!] + } + + input ActorActedInSeriesUpdateConnectionInput { + edge: ActedInUpdateInput + node: SeriesUpdateInput + } + + input ActorActedInSeriesUpdateFieldInput { + connect: [ActorActedInSeriesConnectFieldInput!] + connectOrCreate: [ActorActedInSeriesConnectOrCreateFieldInput!] + create: [ActorActedInSeriesCreateFieldInput!] + delete: [ActorActedInSeriesDeleteFieldInput!] + disconnect: [ActorActedInSeriesDisconnectFieldInput!] + update: ActorActedInSeriesUpdateConnectionInput + where: ActorActedInSeriesConnectionWhere + } + + input ActorActedInUpdateInput { + Movie: [ActorActedInMovieUpdateFieldInput!] + Series: [ActorActedInSeriesUpdateFieldInput!] + } + + type ActorAggregateSelection { + count: Int! + name: StringAggregateSelectionNonNullable! + } + + input ActorConnectInput { + actedIn: ActorActedInConnectInput + } + + input ActorConnectOrCreateInput { + actedIn: ActorActedInConnectOrCreateInput + } + + input ActorCreateInput { + actedIn: ActorActedInCreateInput + name: String! + } + + input ActorDeleteInput { + actedIn: ActorActedInDeleteInput + } + + input ActorDisconnectInput { + actedIn: ActorActedInDisconnectInput + } + + type ActorEdge { + cursor: String! + node: Actor! + } + + input ActorOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more ActorSort objects to sort Actors by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [ActorSort!] + } + + input ActorRelationInput { + actedIn: ActorActedInCreateFieldInput + } + + \\"\\"\\" + Fields to sort Actors by. The order in which sorts are applied is not guaranteed when specifying many fields in one ActorSort object. + \\"\\"\\" + input ActorSort { + name: SortDirection + } + + input ActorUpdateInput { + actedIn: ActorActedInUpdateInput + name: String + } + + input ActorWhere { + AND: [ActorWhere!] + NOT: ActorWhere + OR: [ActorWhere!] + actedIn: ProductionWhere @deprecated(reason: \\"Use \`actedIn_SOME\` instead.\\") + actedInConnection: ActorActedInConnectionWhere @deprecated(reason: \\"Use \`actedInConnection_SOME\` instead.\\") + \\"\\"\\" + Return Actors where all of the related ActorActedInConnections match this filter + \\"\\"\\" + actedInConnection_ALL: ActorActedInConnectionWhere + \\"\\"\\" + Return Actors where none of the related ActorActedInConnections match this filter + \\"\\"\\" + actedInConnection_NONE: ActorActedInConnectionWhere + actedInConnection_NOT: ActorActedInConnectionWhere @deprecated(reason: \\"Use \`actedInConnection_NONE\` instead.\\") + \\"\\"\\" + Return Actors where one of the related ActorActedInConnections match this filter + \\"\\"\\" + actedInConnection_SINGLE: ActorActedInConnectionWhere + \\"\\"\\" + Return Actors where some of the related ActorActedInConnections match this filter + \\"\\"\\" + actedInConnection_SOME: ActorActedInConnectionWhere + \\"\\"\\"Return Actors where all of the related Productions match this filter\\"\\"\\" + actedIn_ALL: ProductionWhere + \\"\\"\\"Return Actors where none of the related Productions match this filter\\"\\"\\" + actedIn_NONE: ProductionWhere + actedIn_NOT: ProductionWhere @deprecated(reason: \\"Use \`actedIn_NONE\` instead.\\") + \\"\\"\\"Return Actors where one of the related Productions match this filter\\"\\"\\" + actedIn_SINGLE: ProductionWhere + \\"\\"\\"Return Actors where some of the related Productions match this filter\\"\\"\\" + actedIn_SOME: ProductionWhere + name: String + name_CONTAINS: String + name_ENDS_WITH: String + name_IN: [String!] + name_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + name_STARTS_WITH: String + } + + type ActorsConnection { + edges: [ActorEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + type CreateActorsMutationResponse { + actors: [Actor!]! + info: CreateInfo! + } + + \\"\\"\\" + Information about the number of nodes and relationships created during a create mutation + \\"\\"\\" + type CreateInfo { + bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") + nodesCreated: Int! + relationshipsCreated: Int! + } + + type CreateMoviesMutationResponse { + info: CreateInfo! + movies: [Movie!]! + } + + type CreateSeriesMutationResponse { + info: CreateInfo! + series: [Series!]! + } + + \\"\\"\\" + Information about the number of nodes and relationships deleted during a delete mutation + \\"\\"\\" + type DeleteInfo { + bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") + nodesDeleted: Int! + relationshipsDeleted: Int! + } + + type Movie { + isan: String! + title: String! + } + + type MovieAggregateSelection { + count: Int! + isan: StringAggregateSelectionNonNullable! + title: StringAggregateSelectionNonNullable! + } + + input MovieConnectOrCreateWhere { + node: MovieUniqueWhere! + } + + input MovieConnectWhere { + node: MovieWhere! + } + + input MovieCreateInput { + isan: String! + title: String! + } + + type MovieEdge { + cursor: String! + node: Movie! + } + + input MovieOnCreateInput { + isan: String! + title: String! + } + + input MovieOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more MovieSort objects to sort Movies by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [MovieSort!] + } + + \\"\\"\\" + Fields to sort Movies by. The order in which sorts are applied is not guaranteed when specifying many fields in one MovieSort object. + \\"\\"\\" + input MovieSort { + isan: SortDirection + title: SortDirection + } + + input MovieUniqueWhere { + isan: String + } + + input MovieUpdateInput { + isan: String + title: String + } + + input MovieWhere { + AND: [MovieWhere!] + NOT: MovieWhere + OR: [MovieWhere!] + isan: String + isan_CONTAINS: String + isan_ENDS_WITH: String + isan_IN: [String!] + isan_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + isan_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + isan_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + isan_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + isan_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + isan_STARTS_WITH: String + title: String + title_CONTAINS: String + title_ENDS_WITH: String + title_IN: [String!] + title_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_STARTS_WITH: String + } + + type MoviesConnection { + edges: [MovieEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + type Mutation { + createActors(input: [ActorCreateInput!]!): CreateActorsMutationResponse! + createMovies(input: [MovieCreateInput!]!): CreateMoviesMutationResponse! + createSeries(input: [SeriesCreateInput!]!): CreateSeriesMutationResponse! + deleteActors(delete: ActorDeleteInput, where: ActorWhere): DeleteInfo! + deleteMovies(where: MovieWhere): DeleteInfo! + deleteSeries(where: SeriesWhere): DeleteInfo! + updateActors(connect: ActorConnectInput, connectOrCreate: ActorConnectOrCreateInput, create: ActorRelationInput, delete: ActorDeleteInput, disconnect: ActorDisconnectInput, update: ActorUpdateInput, where: ActorWhere): UpdateActorsMutationResponse! + updateMovies(update: MovieUpdateInput, where: MovieWhere): UpdateMoviesMutationResponse! + updateSeries(update: SeriesUpdateInput, where: SeriesWhere): UpdateSeriesMutationResponse! + } + + \\"\\"\\"Pagination information (Relay)\\"\\"\\" + type PageInfo { + endCursor: String + hasNextPage: Boolean! + hasPreviousPage: Boolean! + startCursor: String + } + + union Production = Movie | Series + + input ProductionWhere { + Movie: MovieWhere + Series: SeriesWhere + } + + type Query { + actors(options: ActorOptions, where: ActorWhere): [Actor!]! + actorsAggregate(where: ActorWhere): ActorAggregateSelection! + actorsConnection(after: String, first: Int, sort: [ActorSort], where: ActorWhere): ActorsConnection! + movies(options: MovieOptions, where: MovieWhere): [Movie!]! + moviesAggregate(where: MovieWhere): MovieAggregateSelection! + moviesConnection(after: String, first: Int, sort: [MovieSort], where: MovieWhere): MoviesConnection! + productions(options: QueryOptions, where: ProductionWhere): [Production!]! + series(options: SeriesOptions, where: SeriesWhere): [Series!]! + seriesAggregate(where: SeriesWhere): SeriesAggregateSelection! + seriesConnection(after: String, first: Int, sort: [SeriesSort], where: SeriesWhere): SeriesConnection! + } + + \\"\\"\\"Input type for options that can be specified on a query operation.\\"\\"\\" + input QueryOptions { + limit: Int + offset: Int + } + + type Series { + isan: String! + title: String! + } + + type SeriesAggregateSelection { + count: Int! + isan: StringAggregateSelectionNonNullable! + title: StringAggregateSelectionNonNullable! + } + + input SeriesConnectOrCreateWhere { + node: SeriesUniqueWhere! + } + + input SeriesConnectWhere { + node: SeriesWhere! + } + + type SeriesConnection { + edges: [SeriesEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + input SeriesCreateInput { + isan: String! + title: String! + } + + type SeriesEdge { + cursor: String! + node: Series! + } + + input SeriesOnCreateInput { + isan: String! + title: String! + } + + input SeriesOptions { + limit: Int + offset: Int + \\"\\"\\" + Specify one or more SeriesSort objects to sort Series by. The sorts will be applied in the order in which they are arranged in the array. + \\"\\"\\" + sort: [SeriesSort!] + } + + \\"\\"\\" + Fields to sort Series by. The order in which sorts are applied is not guaranteed when specifying many fields in one SeriesSort object. + \\"\\"\\" + input SeriesSort { + isan: SortDirection + title: SortDirection + } + + input SeriesUniqueWhere { + isan: String + } + + input SeriesUpdateInput { + isan: String + title: String + } + + input SeriesWhere { + AND: [SeriesWhere!] + NOT: SeriesWhere + OR: [SeriesWhere!] + isan: String + isan_CONTAINS: String + isan_ENDS_WITH: String + isan_IN: [String!] + isan_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + isan_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + isan_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + isan_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + isan_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + isan_STARTS_WITH: String + title: String + title_CONTAINS: String + title_ENDS_WITH: String + title_IN: [String!] + title_NOT: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_CONTAINS: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_ENDS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_IN: [String!] @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_NOT_STARTS_WITH: String @deprecated(reason: \\"Negation filters will be deprecated, use the NOT operator to achieve the same behavior\\") + title_STARTS_WITH: String + } + + \\"\\"\\"An enum for sorting in either ascending or descending order.\\"\\"\\" + enum SortDirection { + \\"\\"\\"Sort by field values in ascending order.\\"\\"\\" + ASC + \\"\\"\\"Sort by field values in descending order.\\"\\"\\" + DESC + } + + type StringAggregateSelectionNonNullable { + longest: String! + shortest: String! + } + + type UpdateActorsMutationResponse { + actors: [Actor!]! + info: UpdateInfo! + } + + \\"\\"\\" + Information about the number of nodes and relationships created and deleted during an update mutation + \\"\\"\\" + type UpdateInfo { + bookmark: String @deprecated(reason: \\"This field has been deprecated because bookmarks are now handled by the driver.\\") + nodesCreated: Int! + nodesDeleted: Int! + relationshipsCreated: Int! + relationshipsDeleted: Int! + } + + type UpdateMoviesMutationResponse { + info: UpdateInfo! + movies: [Movie!]! + } + + type UpdateSeriesMutationResponse { + info: UpdateInfo! + series: [Series!]! + }" + `); + }); +}); diff --git a/packages/graphql/tests/schema/issues/2377.test.ts b/packages/graphql/tests/schema/issues/2377.test.ts index 6a1291292f3..c10fe0854dd 100644 --- a/packages/graphql/tests/schema/issues/2377.test.ts +++ b/packages/graphql/tests/schema/issues/2377.test.ts @@ -355,6 +355,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => { properties: [Property!] tags: [Tag!] type: ResourceType! + updatedAt: DateTime! } input ResourceDeleteInput { @@ -387,6 +388,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => { properties: [Property!] tags: [Tag!] type: ResourceType! + updatedAt: DateTime! } input ResourceOptions { @@ -437,6 +439,7 @@ describe("https://github.com/neo4j/graphql/issues/2377", () => { input ResourceUpdateInput { containedBy: [ResourceContainedByUpdateFieldInput!] + createdAt: DateTime externalIds: [ID!] externalIds_POP: Int externalIds_PUSH: [ID!] diff --git a/packages/graphql/tests/schema/issues/2993.test.ts b/packages/graphql/tests/schema/issues/2993.test.ts index b12aba88cb4..70090ad2674 100644 --- a/packages/graphql/tests/schema/issues/2993.test.ts +++ b/packages/graphql/tests/schema/issues/2993.test.ts @@ -84,6 +84,10 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { since: SortDirection } + input FOLLOWSUpdateInput { + since: DateTime + } + input FOLLOWSWhere { AND: [FOLLOWSWhere!] NOT: FOLLOWSWhere @@ -330,6 +334,7 @@ describe("https://github.com/neo4j/graphql/issues/2993", () => { } input UserFollowingUpdateConnectionInput { + edge: FOLLOWSUpdateInput node: ProfileUpdateInput } diff --git a/packages/graphql/tests/tck/connections/alias.test.ts b/packages/graphql/tests/tck/connections/alias.test.ts index 156d7f1a41c..d5a6e46e8e6 100644 --- a/packages/graphql/tests/tck/connections/alias.test.ts +++ b/packages/graphql/tests/tck/connections/alias.test.ts @@ -66,12 +66,17 @@ describe("Connections Alias", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { node: { __resolveType: \\"Actor\\", __id: id(this1) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { __resolveType: \\"Actor\\", __id: id(this1) } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { actors: var2 } AS this" + RETURN this { actors: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -111,21 +116,31 @@ describe("Connections Alias", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.name = $param1 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } CALL { WITH this - MATCH (this)<-[this3:ACTED_IN]-(this4:Actor) - WHERE this4.name = $param2 - WITH { screenTime: this3.screenTime, node: { name: this4.name } } AS edge - WITH collect(edge) AS edges + MATCH (this)<-[this4:ACTED_IN]-(this5:Actor) + WHERE this5.name = $param2 + WITH collect({ node: this5, relationship: this4 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var5 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this5, edge.relationship AS this4 + RETURN collect({ screenTime: this4.screenTime, node: { name: this5.name } }) AS var6 + } + RETURN { edges: var6, totalCount: totalCount } AS var7 } - RETURN this { .title, hanks: var2, jenny: var5 } AS this" + RETURN this { .title, hanks: var3, jenny: var7 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/connect-or-create/connect-or-create.test.ts b/packages/graphql/tests/tck/connections/connect-or-create/connect-or-create.test.ts index 55f7b24d239..604e6138df6 100644 --- a/packages/graphql/tests/tck/connections/connect-or-create/connect-or-create.test.ts +++ b/packages/graphql/tests/tck/connections/connect-or-create/connect-or-create.test.ts @@ -17,10 +17,10 @@ * limitations under the License. */ -import { gql } from "graphql-tag"; import type { DocumentNode } from "graphql"; +import gql from "graphql-tag"; import { Neo4jGraphQL } from "../../../../src"; -import { formatCypher, translateQuery, formatParams } from "../../utils/tck-test-utils"; +import { formatCypher, formatParams, translateQuery } from "../../utils/tck-test-utils"; describe("Create or Connect", () => { describe("Simple", () => { @@ -464,7 +464,7 @@ describe("Create or Connect", () => { interface ActedIn @relationshipProperties { id: ID! @id createdAt: DateTime! @timestamp(operations: [CREATE]) - updatedAt: DateTime! @timestamp(operations: [UPDATE]) + updatedAt: DateTime @timestamp(operations: [UPDATE]) screentime: Int! } `; diff --git a/packages/graphql/tests/tck/connections/filtering/composite.test.ts b/packages/graphql/tests/tck/connections/filtering/composite.test.ts index a265d701540..eb457f2527b 100644 --- a/packages/graphql/tests/tck/connections/filtering/composite.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/composite.test.ts @@ -81,12 +81,17 @@ describe("Cypher -> Connections -> Filtering -> Composite", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE ((this1.firstName = $param1 AND this1.lastName = $param2) AND (this0.screenTime > $param3 AND this0.screenTime < $param4)) - WITH { screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -138,12 +143,17 @@ describe("Cypher -> Connections -> Filtering -> Composite", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE (NOT (this1.firstName = $param1 AND this1.lastName = $param2) AND NOT (this0.screenTime < $param3 AND this0.screenTime > $param4)) - WITH { screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -197,12 +207,17 @@ describe("Cypher -> Connections -> Filtering -> Composite", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE ((this1.firstName = $param1 AND this1.lastName = $param2) OR (this0.screenTime > $param3 AND this0.screenTime < $param4)) - WITH { screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -258,12 +273,17 @@ describe("Cypher -> Connections -> Filtering -> Composite", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT ((this1.firstName = $param1 AND this1.lastName = $param2) OR (this0.screenTime > $param3 AND this0.screenTime < $param4)) - WITH { screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -324,12 +344,17 @@ describe("Cypher -> Connections -> Filtering -> Composite", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (((this1.firstName = $param1 AND this1.lastName = $param2) OR (this0.screenTime > $param3 AND this0.screenTime < $param4)) AND (this1.firstName = $param5 AND this1.lastName = $param6)) - WITH { screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/node/and.test.ts b/packages/graphql/tests/tck/connections/filtering/node/and.test.ts index 1244235a023..d5ad9a066b4 100644 --- a/packages/graphql/tests/tck/connections/filtering/node/and.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/node/and.test.ts @@ -75,12 +75,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> AND", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE (this1.firstName = $param0 AND this1.lastName = $param1) - WITH { screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -117,12 +122,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> AND", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this1.firstName = $param0) - WITH { screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/node/arrays.test.ts b/packages/graphql/tests/tck/connections/filtering/node/arrays.test.ts index f259e5afa17..491268beaf6 100644 --- a/packages/graphql/tests/tck/connections/filtering/node/arrays.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/node/arrays.test.ts @@ -74,12 +74,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Arrays", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.name IN $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -117,12 +122,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Arrays", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this1.name IN $param0) - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -161,12 +171,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Arrays", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE $param0 IN this1.favouriteColours - WITH { screenTime: this0.screenTime, node: { name: this1.name, favouriteColours: this1.favouriteColours } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, favouriteColours: this1.favouriteColours } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -202,12 +217,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Arrays", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT ($param0 IN this1.favouriteColours) - WITH { screenTime: this0.screenTime, node: { name: this1.name, favouriteColours: this1.favouriteColours } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, favouriteColours: this1.favouriteColours } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/node/equality.test.ts b/packages/graphql/tests/tck/connections/filtering/node/equality.test.ts index 73631b68e2b..fd33fba8724 100644 --- a/packages/graphql/tests/tck/connections/filtering/node/equality.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/node/equality.test.ts @@ -73,12 +73,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Equality", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.name = $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -113,12 +118,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Equality", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this1.name = $param0) - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/node/numerical.test.ts b/packages/graphql/tests/tck/connections/filtering/node/numerical.test.ts index e14253a4234..0bdc9316130 100644 --- a/packages/graphql/tests/tck/connections/filtering/node/numerical.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/node/numerical.test.ts @@ -75,12 +75,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Numerical", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.age < $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name, age: this1.age } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, age: this1.age } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -119,12 +124,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Numerical", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.age <= $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name, age: this1.age } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, age: this1.age } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -163,12 +173,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Numerical", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.age > $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name, age: this1.age } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, age: this1.age } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -207,12 +222,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Numerical", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.age >= $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name, age: this1.age } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, age: this1.age } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/node/or.test.ts b/packages/graphql/tests/tck/connections/filtering/node/or.test.ts index 8c2281b5915..9a22042b0cf 100644 --- a/packages/graphql/tests/tck/connections/filtering/node/or.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/node/or.test.ts @@ -75,12 +75,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> OR", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE (this1.firstName = $param0 OR this1.lastName = $param1) - WITH { screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { firstName: this1.firstName, lastName: this1.lastName } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/node/points.test.ts b/packages/graphql/tests/tck/connections/filtering/node/points.test.ts index 943728053a2..8459e2474b3 100644 --- a/packages/graphql/tests/tck/connections/filtering/node/points.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/node/points.test.ts @@ -91,15 +91,20 @@ describe("Cypher -> Connections -> Filtering -> Node -> Points", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE point.distance(this1.currentLocation, point($param0.point)) = $param0.distance - WITH { screenTime: this0.screenTime, node: { name: this1.name, currentLocation: CASE - WHEN this1.currentLocation IS NOT NULL THEN { point: this1.currentLocation } - ELSE NULL - END } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, currentLocation: CASE + WHEN this1.currentLocation IS NOT NULL THEN { point: this1.currentLocation } + ELSE NULL + END } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/node/relationship.test.ts b/packages/graphql/tests/tck/connections/filtering/node/relationship.test.ts index 74240205033..39807057a33 100644 --- a/packages/graphql/tests/tck/connections/filtering/node/relationship.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/node/relationship.test.ts @@ -71,12 +71,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> Relationship", () => { MATCH (this1)-[:ACTED_IN]->(this2:Movie) WHERE this2.title = $param0 } - WITH { node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { name: this1.name } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 } - RETURN this { .title, actorsConnection: var3 } AS this" + RETURN this { .title, actorsConnection: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/node/string.test.ts b/packages/graphql/tests/tck/connections/filtering/node/string.test.ts index e372199efe4..4ef8c226361 100644 --- a/packages/graphql/tests/tck/connections/filtering/node/string.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/node/string.test.ts @@ -91,12 +91,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.name CONTAINS $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -131,12 +136,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this1.name CONTAINS $param0) - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -171,12 +181,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.name STARTS WITH $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -211,12 +226,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this1.name STARTS WITH $param0) - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -251,12 +271,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.name ENDS WITH $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -291,12 +316,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this1.name ENDS WITH $param0) - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -331,12 +361,17 @@ describe("Cypher -> Connections -> Filtering -> Node -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.name =~ $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/and.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/and.test.ts index 2a80be5e1f6..f6d9ef839fa 100644 --- a/packages/graphql/tests/tck/connections/filtering/relationship/and.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/relationship/and.test.ts @@ -75,12 +75,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> AND", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE (this0.role ENDS WITH $param0 AND this0.screenTime < $param1) - WITH { role: this0.role, screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ role: this0.role, screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -120,12 +125,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> AND", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this0.role ENDS WITH $param0) - WITH { role: this0.role, screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ role: this0.role, screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/arrays.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/arrays.test.ts index c8839a08528..7c8fae7e14e 100644 --- a/packages/graphql/tests/tck/connections/filtering/relationship/arrays.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/relationship/arrays.test.ts @@ -74,12 +74,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Arrays", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this0.screenTime IN $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -123,12 +128,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Arrays", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this0.screenTime IN $param0) - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -172,12 +182,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Arrays", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE $param0 IN this0.quotes - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -212,12 +227,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Arrays", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT ($param0 IN this0.quotes) - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/equality.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/equality.test.ts index 2c6cf8d5e0d..15a6a60eafe 100644 --- a/packages/graphql/tests/tck/connections/filtering/relationship/equality.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/relationship/equality.test.ts @@ -73,12 +73,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Equality", () => WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this0.screenTime = $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -116,12 +121,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Equality", () => WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this0.screenTime = $param0) - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/numerical.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/numerical.test.ts index ef945d40bc3..01e9fe2f16b 100644 --- a/packages/graphql/tests/tck/connections/filtering/relationship/numerical.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/relationship/numerical.test.ts @@ -73,12 +73,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Numerical", () = WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this0.screenTime < $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -116,12 +121,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Numerical", () = WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this0.screenTime <= $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -159,12 +169,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Numerical", () = WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this0.screenTime > $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -202,12 +217,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Numerical", () = WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this0.screenTime >= $param0 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/or.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/or.test.ts index d287005a98a..236389c1825 100644 --- a/packages/graphql/tests/tck/connections/filtering/relationship/or.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/relationship/or.test.ts @@ -75,12 +75,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> OR", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE (this0.role ENDS WITH $param0 OR this0.screenTime < $param1) - WITH { role: this0.role, screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ role: this0.role, screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/points.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/points.test.ts index ef1ba000150..bc1eec7044e 100644 --- a/packages/graphql/tests/tck/connections/filtering/relationship/points.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/relationship/points.test.ts @@ -89,15 +89,20 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Points", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE point.distance(this0.location, point($param0.point)) = $param0.distance - WITH { screenTime: this0.screenTime, location: CASE - WHEN this0.location IS NOT NULL THEN { point: this0.location } - ELSE NULL - END, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, location: CASE + WHEN this0.location IS NOT NULL THEN { point: this0.location } + ELSE NULL + END, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/string.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/string.test.ts index 342a33b671d..81f6fdf96a2 100644 --- a/packages/graphql/tests/tck/connections/filtering/relationship/string.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/relationship/string.test.ts @@ -91,12 +91,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this0.role CONTAINS $param0 - WITH { role: this0.role, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ role: this0.role, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -131,12 +136,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this0.role CONTAINS $param0) - WITH { role: this0.role, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ role: this0.role, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -171,12 +181,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this0.role STARTS WITH $param0 - WITH { role: this0.role, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ role: this0.role, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -211,12 +226,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this0.role STARTS WITH $param0) - WITH { role: this0.role, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ role: this0.role, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -251,12 +271,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this0.role ENDS WITH $param0 - WITH { role: this0.role, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ role: this0.role, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -291,12 +316,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE NOT (this0.role ENDS WITH $param0) - WITH { role: this0.role, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ role: this0.role, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -331,12 +361,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> String", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this0.role =~ $param0 - WITH { role: this0.role, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ role: this0.role, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/filtering/relationship/temporal.test.ts b/packages/graphql/tests/tck/connections/filtering/relationship/temporal.test.ts index 88af9c47f6f..8007ca2a7a1 100644 --- a/packages/graphql/tests/tck/connections/filtering/relationship/temporal.test.ts +++ b/packages/graphql/tests/tck/connections/filtering/relationship/temporal.test.ts @@ -77,12 +77,17 @@ describe("Cypher -> Connections -> Filtering -> Relationship -> Temporal", () => WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE (this0.startDate > $param0 AND this0.endDateTime < $param1) - WITH { startDate: this0.startDate, endDateTime: apoc.date.convertFormat(toString(this0.endDateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ startDate: this0.startDate, endDateTime: apoc.date.convertFormat(toString(this0.endDateTime), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/mixed-nesting.test.ts b/packages/graphql/tests/tck/connections/mixed-nesting.test.ts index 05ed6d7e7ca..7b286002e6b 100644 --- a/packages/graphql/tests/tck/connections/mixed-nesting.test.ts +++ b/packages/graphql/tests/tck/connections/mixed-nesting.test.ts @@ -77,19 +77,24 @@ describe("Mixed nesting", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.name = $param1 + WITH collect({ node: this1, relationship: this0 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this1 - MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) - WHERE NOT (this3.title = $param2) - WITH this3 { .title } AS this3 - RETURN collect(this3) AS var4 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + CALL { + WITH this1 + MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) + WHERE NOT (this3.title = $param2) + WITH this3 { .title } AS this3 + RETURN collect(this3) AS var4 + } + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, movies: var4 } }) AS var5 } - WITH { screenTime: this0.screenTime, node: { name: this1.name, movies: var4 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var5 + RETURN { edges: var5, totalCount: totalCount } AS var6 } - RETURN this { .title, actorsConnection: var5 } AS this" + RETURN this { .title, actorsConnection: var6 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -137,28 +142,38 @@ describe("Mixed nesting", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.name = $param1 + WITH collect({ node: this1, relationship: this0 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this1 - MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) - WHERE NOT (this3.title = $param2) + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 CALL { - WITH this3 - MATCH (this3)<-[this4:ACTED_IN]-(this5:Actor) - WHERE NOT (this5.name = $param3) - WITH this5 { .name } AS this5 - RETURN collect(this5) AS var6 + WITH this1 + MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) + WHERE NOT (this3.title = $param2) + WITH collect({ node: this3, relationship: this2 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this3, edge.relationship AS this2 + CALL { + WITH this3 + MATCH (this3)<-[this4:ACTED_IN]-(this5:Actor) + WHERE NOT (this5.name = $param3) + WITH this5 { .name } AS this5 + RETURN collect(this5) AS var6 + } + RETURN collect({ node: { title: this3.title, actors: var6 } }) AS var7 + } + RETURN { edges: var7, totalCount: totalCount } AS var8 } - WITH { node: { title: this3.title, actors: var6 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var7 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, moviesConnection: var8 } }) AS var9 } - WITH { screenTime: this0.screenTime, node: { name: this1.name, moviesConnection: var7 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var8 + RETURN { edges: var9, totalCount: totalCount } AS var10 } - RETURN this { .title, actorsConnection: var8 } AS this" + RETURN this { .title, actorsConnection: var10 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -204,15 +219,20 @@ describe("Mixed nesting", () => { WITH this1 MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) WHERE NOT (this3.title = $param2) - WITH { screenTime: this2.screenTime, node: { title: this3.title } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this3, relationship: this2 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var4 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this3, edge.relationship AS this2 + RETURN collect({ screenTime: this2.screenTime, node: { title: this3.title } }) AS var4 + } + RETURN { edges: var4, totalCount: totalCount } AS var5 } - WITH this1 { .name, moviesConnection: var4 } AS this1 - RETURN collect(this1) AS var5 + WITH this1 { .name, moviesConnection: var5 } AS this1 + RETURN collect(this1) AS var6 } - RETURN this { .title, actors: var5 } AS this" + RETURN this { .title, actors: var6 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/projections/create.test.ts b/packages/graphql/tests/tck/connections/projections/create.test.ts index 5a7e052517b..dde6173cd36 100644 --- a/packages/graphql/tests/tck/connections/projections/create.test.ts +++ b/packages/graphql/tests/tck/connections/projections/create.test.ts @@ -81,12 +81,17 @@ describe("Cypher -> Connections -> Projections -> Create", () => { CALL { WITH create_this1 MATCH (create_this1)<-[create_this2:ACTED_IN]-(create_this3:Actor) - WITH { screenTime: create_this2.screenTime, node: { name: create_this3.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this3, relationship: create_this2 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var4 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this3, edge.relationship AS create_this2 + RETURN collect({ screenTime: create_this2.screenTime, node: { name: create_this3.name } }) AS create_var4 + } + RETURN { edges: create_var4, totalCount: totalCount } AS create_var5 } - RETURN collect(create_this1 { .title, actorsConnection: create_var4 }) AS data" + RETURN collect(create_this1 { .title, actorsConnection: create_var5 }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -134,12 +139,17 @@ describe("Cypher -> Connections -> Projections -> Create", () => { CALL { WITH create_this1 MATCH (create_this1)<-[create_this2:ACTED_IN]-(create_this3:Actor) - WITH { screenTime: create_this2.screenTime, node: { name: create_this3.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this3, relationship: create_this2 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var4 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this3, edge.relationship AS create_this2 + RETURN collect({ screenTime: create_this2.screenTime, node: { name: create_this3.name } }) AS create_var4 + } + RETURN { edges: create_var4, totalCount: totalCount } AS create_var5 } - RETURN collect(create_this1 { .title, actorsConnection: create_var4 }) AS data" + RETURN collect(create_this1 { .title, actorsConnection: create_var5 }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -191,12 +201,17 @@ describe("Cypher -> Connections -> Projections -> Create", () => { WITH create_this1 MATCH (create_this1)<-[create_this2:ACTED_IN]-(create_this3:Actor) WHERE create_this3.name = $create_param1 - WITH { screenTime: create_this2.screenTime, node: { name: create_this3.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this3, relationship: create_this2 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var4 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this3, edge.relationship AS create_this2 + RETURN collect({ screenTime: create_this2.screenTime, node: { name: create_this3.name } }) AS create_var4 + } + RETURN { edges: create_var4, totalCount: totalCount } AS create_var5 } - RETURN collect(create_this1 { .title, actorsConnection: create_var4 }) AS data" + RETURN collect(create_this1 { .title, actorsConnection: create_var5 }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/projections/projections.test.ts b/packages/graphql/tests/tck/connections/projections/projections.test.ts index f1ecf4801af..d1de0a310ec 100644 --- a/packages/graphql/tests/tck/connections/projections/projections.test.ts +++ b/packages/graphql/tests/tck/connections/projections/projections.test.ts @@ -71,12 +71,17 @@ describe("Relay Cursor Connection projections", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { node: { __resolveType: \\"Actor\\", __id: id(this1) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { __resolveType: \\"Actor\\", __id: id(this1) } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -111,12 +116,17 @@ describe("Relay Cursor Connection projections", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { node: { __resolveType: \\"Actor\\", __id: id(this1) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { __resolveType: \\"Actor\\", __id: id(this1) } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -146,18 +156,17 @@ describe("Relay Cursor Connection projections", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { node: { __resolveType: \\"Actor\\", __id: id(this1) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge + WITH edge.node AS this1, edge.relationship AS this0 + WITH * LIMIT $param1 - RETURN collect(edge) AS var2 + RETURN collect({ node: { __resolveType: \\"Actor\\", __id: id(this1) } }) AS var2 } - WITH var2 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN { edges: var2, totalCount: totalCount } AS var3 } RETURN this { .title, actorsConnection: var3 } AS this" `); @@ -291,12 +300,17 @@ describe("Relay Cursor Connection projections", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -331,18 +345,17 @@ describe("Relay Cursor Connection projections", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge + WITH edge.node AS this1, edge.relationship AS this0 + WITH * LIMIT $param1 - RETURN collect(edge) AS var2 + RETURN collect({ node: { name: this1.name } }) AS var2 } - WITH var2 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN { edges: var2, totalCount: totalCount } AS var3 } RETURN this { .title, actorsConnection: var3 } AS this" `); diff --git a/packages/graphql/tests/tck/connections/relationship-properties.test.ts b/packages/graphql/tests/tck/connections/relationship-properties.test.ts index 71b8f23b614..1a53442b58d 100644 --- a/packages/graphql/tests/tck/connections/relationship-properties.test.ts +++ b/packages/graphql/tests/tck/connections/relationship-properties.test.ts @@ -74,12 +74,17 @@ describe("Relationship Properties Cypher", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -115,12 +120,17 @@ describe("Relationship Properties Cypher", () => { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) WHERE this1.name = $param1 - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -156,20 +166,17 @@ describe("Relationship Properties Cypher", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH this0, this1 - ORDER BY this0.screenTime DESC - WITH { screenTime: this0.screenTime, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge - ORDER BY edge.screenTime DESC - RETURN collect(edge) AS var2 + WITH edge.node AS this1, edge.relationship AS this0 + WITH * + ORDER BY this0.screenTime DESC + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name } }) AS var2 } - WITH var2 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN { edges: var2, totalCount: totalCount } AS var3 } RETURN this { .title, actorsConnection: var3 } AS this" `); @@ -204,20 +211,17 @@ describe("Relationship Properties Cypher", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH this0, this1 - ORDER BY this0.year DESC, this1.name ASC - WITH { year: this0.year, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge - ORDER BY edge.year DESC, edge.node.name ASC - RETURN collect(edge) AS var2 + WITH edge.node AS this1, edge.relationship AS this0 + WITH * + ORDER BY this0.year DESC, this1.name ASC + RETURN collect({ year: this0.year, node: { name: this1.name } }) AS var2 } - WITH var2 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN { edges: var2, totalCount: totalCount } AS var3 } RETURN this { actorsConnection: var3 } AS this" `); @@ -247,20 +251,17 @@ describe("Relationship Properties Cypher", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH this0, this1 - ORDER BY this1.name ASC, this0.year DESC - WITH { year: this0.year, node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge - ORDER BY edge.node.name ASC, edge.year DESC - RETURN collect(edge) AS var2 + WITH edge.node AS this1, edge.relationship AS this0 + WITH * + ORDER BY this1.name ASC, this0.year DESC + RETURN collect({ year: this0.year, node: { name: this1.name } }) AS var2 } - WITH var2 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN { edges: var2, totalCount: totalCount } AS var3 } RETURN this { actorsConnection: var3 } AS this" `); @@ -301,20 +302,30 @@ describe("Relationship Properties Cypher", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WITH collect({ node: this1, relationship: this0 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this1 - MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) - WITH { screenTime: this2.screenTime, node: { title: this3.title } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var4 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + CALL { + WITH this1 + MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) + WITH collect({ node: this3, relationship: this2 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this3, edge.relationship AS this2 + RETURN collect({ screenTime: this2.screenTime, node: { title: this3.title } }) AS var4 + } + RETURN { edges: var4, totalCount: totalCount } AS var5 + } + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, moviesConnection: var5 } }) AS var6 } - WITH { screenTime: this0.screenTime, node: { name: this1.name, moviesConnection: var4 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var5 + RETURN { edges: var6, totalCount: totalCount } AS var7 } - RETURN this { .title, actorsConnection: var5 } AS this" + RETURN this { .title, actorsConnection: var7 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -365,28 +376,43 @@ describe("Relationship Properties Cypher", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) + WITH collect({ node: this1, relationship: this0 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this1 - MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 CALL { - WITH this3 - MATCH (this3)<-[this4:ACTED_IN]-(this5:Actor) - WITH { screenTime: this4.screenTime, node: { name: this5.name } } AS edge - WITH collect(edge) AS edges + WITH this1 + MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) + WITH collect({ node: this3, relationship: this2 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var6 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this3, edge.relationship AS this2 + CALL { + WITH this3 + MATCH (this3)<-[this4:ACTED_IN]-(this5:Actor) + WITH collect({ node: this5, relationship: this4 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this5, edge.relationship AS this4 + RETURN collect({ screenTime: this4.screenTime, node: { name: this5.name } }) AS var6 + } + RETURN { edges: var6, totalCount: totalCount } AS var7 + } + RETURN collect({ screenTime: this2.screenTime, node: { title: this3.title, actorsConnection: var7 } }) AS var8 + } + RETURN { edges: var8, totalCount: totalCount } AS var9 } - WITH { screenTime: this2.screenTime, node: { title: this3.title, actorsConnection: var6 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var7 + RETURN collect({ screenTime: this0.screenTime, node: { name: this1.name, moviesConnection: var9 } }) AS var10 } - WITH { screenTime: this0.screenTime, node: { name: this1.name, moviesConnection: var7 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var8 + RETURN { edges: var10, totalCount: totalCount } AS var11 } - RETURN this { .title, actorsConnection: var8 } AS this" + RETURN this { .title, actorsConnection: var11 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/relationship_properties/connect.test.ts b/packages/graphql/tests/tck/connections/relationship_properties/connect.test.ts index 7ec6fde941a..6d5a5329aa1 100644 --- a/packages/graphql/tests/tck/connections/relationship_properties/connect.test.ts +++ b/packages/graphql/tests/tck/connections/relationship_properties/connect.test.ts @@ -98,14 +98,19 @@ describe("Relationship Properties Connect Cypher", () => { CALL { WITH this0 MATCH (this0)<-[create_this0:ACTED_IN]-(create_this1:Actor) - WITH { screenTime: create_this0.screenTime, node: { name: create_this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this1, relationship: create_this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this1, edge.relationship AS create_this0 + RETURN collect({ screenTime: create_this0.screenTime, node: { name: create_this1.name } }) AS create_var2 + } + RETURN { edges: create_var2, totalCount: totalCount } AS create_var3 } - RETURN this0 { .title, actorsConnection: create_var2 } AS create_var3 + RETURN this0 { .title, actorsConnection: create_var3 } AS create_var4 } - RETURN [create_var3] AS data" + RETURN [create_var4] AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -178,14 +183,19 @@ describe("Relationship Properties Connect Cypher", () => { CALL { WITH this0 MATCH (this0)<-[create_this0:ACTED_IN]-(create_this1:Actor) - WITH { screenTime: create_this0.screenTime, node: { name: create_this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this1, relationship: create_this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this1, edge.relationship AS create_this0 + RETURN collect({ screenTime: create_this0.screenTime, node: { name: create_this1.name } }) AS create_var2 + } + RETURN { edges: create_var2, totalCount: totalCount } AS create_var3 } - RETURN this0 { .title, actorsConnection: create_var2 } AS create_var3 + RETURN this0 { .title, actorsConnection: create_var3 } AS create_var4 } - RETURN [create_var3] AS data" + RETURN [create_var4] AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/relationship_properties/create.test.ts b/packages/graphql/tests/tck/connections/relationship_properties/create.test.ts index a7b01afb791..9a11eebdedb 100644 --- a/packages/graphql/tests/tck/connections/relationship_properties/create.test.ts +++ b/packages/graphql/tests/tck/connections/relationship_properties/create.test.ts @@ -101,12 +101,17 @@ describe("Relationship Properties Create Cypher", () => { CALL { WITH create_this1 MATCH (create_this1)<-[create_this8:ACTED_IN]-(create_this9:Actor) - WITH { screenTime: create_this8.screenTime, node: { name: create_this9.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this9, relationship: create_this8 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var10 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this9, edge.relationship AS create_this8 + RETURN collect({ screenTime: create_this8.screenTime, node: { name: create_this9.name } }) AS create_var10 + } + RETURN { edges: create_var10, totalCount: totalCount } AS create_var11 } - RETURN collect(create_this1 { .title, actorsConnection: create_var10 }) AS data" + RETURN collect(create_this1 { .title, actorsConnection: create_var11 }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/connections/sort.test.ts b/packages/graphql/tests/tck/connections/sort.test.ts index 76a14bb9e7a..57d8ecf9aa2 100644 --- a/packages/graphql/tests/tck/connections/sort.test.ts +++ b/packages/graphql/tests/tck/connections/sort.test.ts @@ -77,25 +77,32 @@ describe("Relationship Properties Cypher", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH collect(this) AS edges + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount - WITH * - ORDER BY this.title ASC - LIMIT $param0 CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { node: { name: this1.name } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.title ASC + LIMIT $param0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 + } + RETURN collect({ node: { title: this0.title, actorsConnection: var4 } }) AS var5 } - WITH { node: this { .title, actorsConnection: var2 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var5, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -131,35 +138,42 @@ describe("Relationship Properties Cypher", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH collect(this) AS edges + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 CALL { - WITH this - WITH this AS this - MATCH (actor:Actor)-[:ACTED_IN]->(this) RETURN count(actor) as count + WITH this0 + CALL { + WITH this0 + WITH this0 AS this + MATCH (actor:Actor)-[:ACTED_IN]->(this) RETURN count(actor) as count + } + UNWIND count AS this1 + RETURN head(collect(this1)) AS this1 } - UNWIND count AS this0 - RETURN head(collect(this0)) AS this0 - } - WITH * - ORDER BY this.title DESC, this0 ASC - LIMIT $param0 - CALL { - WITH this - MATCH (this)<-[this1:ACTED_IN]-(this2:Actor) - WITH { node: { name: this2.name } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + WITH * + ORDER BY this0.title DESC, this1 ASC + LIMIT $param0 + CALL { + WITH this0 + MATCH (this0)<-[this2:ACTED_IN]-(this3:Actor) + WITH collect({ node: this3, relationship: this2 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this3, edge.relationship AS this2 + RETURN collect({ node: { name: this3.name } }) AS var4 + } + RETURN { edges: var4, totalCount: totalCount } AS var5 + } + RETURN collect({ node: { title: this0.title, actorsConnection: var5 } }) AS var6 } - WITH { node: this { .title, actorsConnection: var3, numberOfActors: this0 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var6, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/directives/alias.test.ts b/packages/graphql/tests/tck/directives/alias.test.ts index c93bcede695..5324df4c731 100644 --- a/packages/graphql/tests/tck/directives/alias.test.ts +++ b/packages/graphql/tests/tck/directives/alias.test.ts @@ -107,12 +107,17 @@ describe("Cypher alias directive", () => { CALL { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - WITH { character: this0.characterPropInDb, screenTime: this0.screenTime, node: { title: this1.title, rating: this1.ratingPropInDb } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ character: this0.characterPropInDb, screenTime: this0.screenTime, node: { title: this1.title, rating: this1.ratingPropInDb } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .name, city: this.cityPropInDb, actedInConnection: var2 } AS this" + RETURN this { .name, city: this.cityPropInDb, actedInConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -193,12 +198,17 @@ describe("Cypher alias directive", () => { CALL { WITH create_this1 MATCH (create_this1)-[create_this11:ACTED_IN]->(create_this12:Movie) - WITH { character: create_this11.characterPropInDb, screenTime: create_this11.screenTime, node: { title: create_this12.title, rating: create_this12.ratingPropInDb } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this12, relationship: create_this11 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var13 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this12, edge.relationship AS create_this11 + RETURN collect({ character: create_this11.characterPropInDb, screenTime: create_this11.screenTime, node: { title: create_this12.title, rating: create_this12.ratingPropInDb } }) AS create_var13 + } + RETURN { edges: create_var13, totalCount: totalCount } AS create_var14 } - RETURN collect(create_this1 { .name, city: create_this1.cityPropInDb, actedIn: create_var10, actedInConnection: create_var13 }) AS data" + RETURN collect(create_this1 { .name, city: create_this1.cityPropInDb, actedIn: create_var10, actedInConnection: create_var14 }) AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/bind/interface-relationships/implementation-bind.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/bind/interface-relationships/implementation-bind.test.ts index 9b1431e2a35..a0c6f8e72da 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/bind/interface-relationships/implementation-bind.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/bind/interface-relationships/implementation-bind.test.ts @@ -139,10 +139,10 @@ describe("Cypher Auth Allow", () => { RETURN c AS this0_contentPost0_node_creator_User_unique_ignored } WITH * - OPTIONAL MATCH (this0_contentPost0_node)<-[:HAS_CONTENT]-(authorization_1_2_1_0_after_this0:User) - WITH *, count(authorization_1_2_1_0_after_this0) AS creatorCount + OPTIONAL MATCH (this0_contentPost0_node)<-[:HAS_CONTENT]-(authorization_0_2_0_1_after_this0:User) + WITH *, count(authorization_0_2_0_1_after_this0) AS creatorCount WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0_contentPost0_node_creator0_node.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_1_2_1_0_after_this0.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0_contentPost0_node_creator0_node.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_0_2_0_1_after_this0.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this0 } CALL { diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts index c49b733e228..c28bcb379ec 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/roles-where.test.ts @@ -232,12 +232,17 @@ describe("Cypher Auth Where with Roles", () => { WITH this MATCH (this)-[this0:HAS_POST]->(this1:Post) WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND single(this2 IN [(this1)<-[:HAS_POST]-(this2:User) WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) | 1] WHERE true) AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { content: this1.content } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { content: this1.content } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 } - RETURN this { .id, postsConnection: var3 } AS this" + RETURN this { .id, postsConnection: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -286,12 +291,17 @@ describe("Cypher Auth Where with Roles", () => { WITH this MATCH (this)-[this0:HAS_POST]->(this1:Post) WHERE (this1.id = $param4 AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND single(this2 IN [(this1)<-[:HAS_POST]-(this2:User) WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) | 1] WHERE true) AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - WITH { node: { content: this1.content } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { content: this1.content } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 } - RETURN this { .id, postsConnection: var3 } AS this" + RETURN this { .id, postsConnection: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -822,7 +832,7 @@ describe("Cypher Auth Where with Roles", () => { CALL { WITH this0 OPTIONAL MATCH (this0_posts_connect0_node:Post) - WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND single(authorization_0_0_0_0_before_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_0_0_0_before_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_0_0_0_0_before_this0.id = $jwt.sub) | 1] WHERE true) AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_before_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_before_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND single(authorization_0_before_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_before_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_0_before_this0.id = $jwt.sub) | 1] WHERE true) AND ($jwt.roles IS NOT NULL AND $authorization_0_before_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_before_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) CALL { WITH * WITH collect(this0_posts_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -835,11 +845,11 @@ describe("Cypher Auth Where with Roles", () => { } WITH this0, this0_posts_connect0_node WITH this0, this0_posts_connect0_node - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND single(authorization_0_0_0_0_after_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_0_0_0_after_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_0_0_0_0_after_this0.id = $jwt.sub) | 1] WHERE true) AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND single(authorization_0_after_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_after_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_0_after_this0.id = $jwt.sub) | 1] WHERE true) AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) RETURN count(*) AS connect_this0_posts_connect_Post0 } WITH * - WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this0 } CALL { @@ -861,12 +871,12 @@ describe("Cypher Auth Where with Roles", () => { ], \\"sub\\": \\"id-01\\" }, - \\"authorization_0_0_0_0_before_param2\\": \\"user\\", - \\"authorization_0_0_0_0_before_param3\\": \\"admin\\", - \\"authorization_0_0_0_0_after_param2\\": \\"user\\", - \\"authorization_0_0_0_0_after_param3\\": \\"admin\\", - \\"authorization_0_0_0_0_after_param4\\": \\"user\\", - \\"authorization_0_0_0_0_after_param5\\": \\"admin\\", + \\"authorization_0_before_param2\\": \\"user\\", + \\"authorization_0_before_param3\\": \\"admin\\", + \\"authorization_0_after_param2\\": \\"user\\", + \\"authorization_0_after_param3\\": \\"admin\\", + \\"authorization_0_after_param4\\": \\"user\\", + \\"authorization_0_after_param5\\": \\"admin\\", \\"resolvedCallbacks\\": {} }" `); @@ -907,7 +917,7 @@ describe("Cypher Auth Where with Roles", () => { CALL { WITH this0 OPTIONAL MATCH (this0_posts_connect0_node:Post) - WHERE this0_posts_connect0_node.id = $this0_posts_connect0_node_param0 AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND single(authorization_0_0_0_0_before_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_0_0_0_before_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_0_0_0_0_before_this0.id = $jwt.sub) | 1] WHERE true) AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_before_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_before_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE this0_posts_connect0_node.id = $this0_posts_connect0_node_param0 AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND single(authorization_0_before_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_before_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_0_before_this0.id = $jwt.sub) | 1] WHERE true) AND ($jwt.roles IS NOT NULL AND $authorization_0_before_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_before_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) CALL { WITH * WITH collect(this0_posts_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -920,11 +930,11 @@ describe("Cypher Auth Where with Roles", () => { } WITH this0, this0_posts_connect0_node WITH this0, this0_posts_connect0_node - WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND single(authorization_0_0_0_0_after_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_0_0_0_after_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_0_0_0_0_after_this0.id = $jwt.sub) | 1] WHERE true) AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE (apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND single(authorization_0_after_this0 IN [(this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_after_this0:User) WHERE ($jwt.sub IS NOT NULL AND authorization_0_after_this0.id = $jwt.sub) | 1] WHERE true) AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param4 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param5 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) RETURN count(*) AS connect_this0_posts_connect_Post0 } WITH * - WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT (($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this0 } CALL { @@ -947,12 +957,12 @@ describe("Cypher Auth Where with Roles", () => { ], \\"sub\\": \\"id-01\\" }, - \\"authorization_0_0_0_0_before_param2\\": \\"user\\", - \\"authorization_0_0_0_0_before_param3\\": \\"admin\\", - \\"authorization_0_0_0_0_after_param2\\": \\"user\\", - \\"authorization_0_0_0_0_after_param3\\": \\"admin\\", - \\"authorization_0_0_0_0_after_param4\\": \\"user\\", - \\"authorization_0_0_0_0_after_param5\\": \\"admin\\", + \\"authorization_0_before_param2\\": \\"user\\", + \\"authorization_0_before_param3\\": \\"admin\\", + \\"authorization_0_after_param2\\": \\"user\\", + \\"authorization_0_after_param3\\": \\"admin\\", + \\"authorization_0_after_param4\\": \\"user\\", + \\"authorization_0_after_param5\\": \\"admin\\", \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/where/connection-auth-filter.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/where/connection-auth-filter.test.ts index 0cd74be7fda..01a668a9e8a 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/where/connection-auth-filter.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/where/connection-auth-filter.test.ts @@ -17,11 +17,11 @@ * limitations under the License. */ -import { gql } from "graphql-tag"; import type { DocumentNode } from "graphql"; +import { gql } from "graphql-tag"; import { Neo4jGraphQL } from "../../../../../../src"; -import { formatCypher, translateQuery, formatParams } from "../../../../utils/tck-test-utils"; import { createBearerToken } from "../../../../../utils/create-bearer-token"; +import { formatCypher, formatParams, translateQuery } from "../../../../utils/tck-test-utils"; describe("Connection auth filter", () => { const secret = "secret"; @@ -91,15 +91,17 @@ describe("Connection auth filter", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount - WITH { node: this { .id } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { id: this0.id } }) AS var1 + } + RETURN { edges: var1, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -134,15 +136,17 @@ describe("Connection auth filter", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE (this.name = $param0 AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub))) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE (this0.name = $param0 AND ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub))) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount - WITH { node: this { .id } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { id: this0.id } }) AS var1 + } + RETURN { edges: var1, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -181,26 +185,28 @@ describe("Connection auth filter", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this - MATCH (this)-[this0:HAS_POST]->(this1:Post) - OPTIONAL MATCH (this1)<-[:HAS_POST]-(this2:User) - WITH *, count(this2) AS creatorCount - WITH * - WITH * - WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))) - WITH this1 { .content } AS this1 - RETURN collect(this1) AS var3 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:HAS_POST]->(this2:Post) + OPTIONAL MATCH (this2)<-[:HAS_POST]-(this3:User) + WITH *, count(this3) AS creatorCount + WITH * + WITH * + WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub))) + WITH this2 { .content } AS this2 + RETURN collect(this2) AS var4 + } + RETURN collect({ node: { id: this0.id, posts: var4 } }) AS var5 } - WITH { node: this { .id, posts: var3 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var5, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -242,27 +248,34 @@ describe("Connection auth filter", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this - MATCH (this)-[this0:HAS_POST]->(this1:Post) - OPTIONAL MATCH (this1)<-[:HAS_POST]-(this2:User) - WITH *, count(this2) AS creatorCount - WITH * - WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))) - WITH { node: { content: this1.content } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:HAS_POST]->(this2:Post) + OPTIONAL MATCH (this2)<-[:HAS_POST]-(this3:User) + WITH *, count(this3) AS creatorCount + WITH * + WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub))) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { content: this2.content } }) AS var4 + } + RETURN { edges: var4, totalCount: totalCount } AS var5 + } + RETURN collect({ node: { id: this0.id, postsConnection: var5 } }) AS var6 } - WITH { node: this { .id, postsConnection: var3 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var6, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -304,27 +317,34 @@ describe("Connection auth filter", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this - MATCH (this)-[this0:HAS_POST]->(this1:Post) - OPTIONAL MATCH (this1)<-[:HAS_POST]-(this2:User) - WITH *, count(this2) AS creatorCount - WITH * - WHERE (this1.id = $param2 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub)))) - WITH { node: { content: this1.content } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:HAS_POST]->(this2:Post) + OPTIONAL MATCH (this2)<-[:HAS_POST]-(this3:User) + WITH *, count(this3) AS creatorCount + WITH * + WHERE (this2.id = $param2 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub)))) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { content: this2.content } }) AS var4 + } + RETURN { edges: var4, totalCount: totalCount } AS var5 + } + RETURN collect({ node: { id: this0.id, postsConnection: var5 } }) AS var6 } - WITH { node: this { .id, postsConnection: var3 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var6, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -363,26 +383,28 @@ describe("Connection auth filter", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this - MATCH (this)-[this0:HAS_POST]->(this1:Post) - OPTIONAL MATCH (this1)<-[:HAS_POST]-(this2:User) - WITH *, count(this2) AS creatorCount - WITH * - WITH * - WHERE (this1.content = $param2 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub)))) - WITH this1 { .content } AS this1 - RETURN collect(this1) AS var3 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:HAS_POST]->(this2:Post) + OPTIONAL MATCH (this2)<-[:HAS_POST]-(this3:User) + WITH *, count(this3) AS creatorCount + WITH * + WITH * + WHERE (this2.content = $param2 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub)))) + WITH this2 { .content } AS this2 + RETURN collect(this2) AS var4 + } + RETURN collect({ node: { id: this0.id, posts: var4 } }) AS var5 } - WITH { node: this { .id, posts: var3 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var5, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -423,30 +445,32 @@ describe("Connection auth filter", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 CALL { - WITH * - MATCH (this)-[this0:HAS_POST]->(this1:Post) - OPTIONAL MATCH (this1)<-[:HAS_POST]-(this2:User) - WITH *, count(this2) AS creatorCount - WITH * - WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))) - WITH this1 { .id, __resolveType: \\"Post\\", __id: id(this1) } AS this1 - RETURN this1 AS var3 + WITH this0 + CALL { + WITH * + MATCH (this0)-[this1:HAS_POST]->(this2:Post) + OPTIONAL MATCH (this2)<-[:HAS_POST]-(this3:User) + WITH *, count(this3) AS creatorCount + WITH * + WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub))) + WITH this2 { .id, __resolveType: \\"Post\\", __id: id(this2) } AS this2 + RETURN this2 AS var4 + } + WITH var4 + RETURN collect(var4) AS var4 } - WITH var3 - RETURN collect(var3) AS var3 + RETURN collect({ node: { id: this0.id, content: var4 } }) AS var5 } - WITH { node: this { .id, content: var3 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var5, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -490,31 +514,33 @@ describe("Connection auth filter", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 CALL { - WITH this - MATCH (this)-[this0:HAS_POST]->(this1:Post) - OPTIONAL MATCH (this1)<-[:HAS_POST]-(this2:User) - WITH *, count(this2) AS creatorCount - WITH * - WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))) - WITH { node: { __resolveType: \\"Post\\", __id: id(this1), id: this1.id } } AS edge - RETURN edge + WITH this0 + CALL { + WITH this0 + MATCH (this0)-[this1:HAS_POST]->(this2:Post) + OPTIONAL MATCH (this2)<-[:HAS_POST]-(this3:User) + WITH *, count(this3) AS creatorCount + WITH * + WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub))) + WITH { node: { __resolveType: \\"Post\\", __id: id(this2), id: this2.id } } AS edge + RETURN edge + } + WITH collect(edge) AS edges + WITH edges, size(edges) AS totalCount + RETURN { edges: edges, totalCount: totalCount } AS var4 } - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN collect({ node: { id: this0.id, contentConnection: var4 } }) AS var5 } - WITH { node: this { .id, contentConnection: var3 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var5, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -558,31 +584,33 @@ describe("Connection auth filter", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 CALL { - WITH this - MATCH (this)-[this0:HAS_POST]->(this1:Post) - OPTIONAL MATCH (this1)<-[:HAS_POST]-(this2:User) - WITH *, count(this2) AS creatorCount - WITH * - WHERE (this1.id = $param2 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub)))) - WITH { node: { __resolveType: \\"Post\\", __id: id(this1), id: this1.id } } AS edge - RETURN edge + WITH this0 + CALL { + WITH this0 + MATCH (this0)-[this1:HAS_POST]->(this2:Post) + OPTIONAL MATCH (this2)<-[:HAS_POST]-(this3:User) + WITH *, count(this3) AS creatorCount + WITH * + WHERE (this2.id = $param2 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub)))) + WITH { node: { __resolveType: \\"Post\\", __id: id(this2), id: this2.id } } AS edge + RETURN edge + } + WITH collect(edge) AS edges + WITH edges, size(edges) AS totalCount + RETURN { edges: edges, totalCount: totalCount } AS var4 } - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN collect({ node: { id: this0.id, contentConnection: var4 } }) AS var5 } - WITH { node: this { .id, contentConnection: var3 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var5, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts b/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts index 55f41370f5a..11f5d530161 100644 --- a/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/arguments/where/where.test.ts @@ -17,11 +17,11 @@ * limitations under the License. */ -import { gql } from "graphql-tag"; import type { DocumentNode } from "graphql"; +import { gql } from "graphql-tag"; import { Neo4jGraphQL } from "../../../../../../src"; -import { formatCypher, translateQuery, formatParams } from "../../../../utils/tck-test-utils"; import { createBearerToken } from "../../../../../utils/create-bearer-token"; +import { formatCypher, formatParams, translateQuery } from "../../../../utils/tck-test-utils"; describe("Cypher Auth Where", () => { const secret = "secret"; @@ -221,12 +221,17 @@ describe("Cypher Auth Where", () => { WITH *, count(this2) AS creatorCount WITH * WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))) - WITH { node: { content: this1.content } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { content: this1.content } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 } - RETURN this { .id, postsConnection: var3 } AS this" + RETURN this { .id, postsConnection: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -274,12 +279,17 @@ describe("Cypher Auth Where", () => { WITH *, count(this2) AS creatorCount WITH * WHERE (this1.id = $param2 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub)))) - WITH { node: { content: this1.content } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { content: this1.content } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 } - RETURN this { .id, postsConnection: var3 } AS this" + RETURN this { .id, postsConnection: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -814,10 +824,10 @@ describe("Cypher Auth Where", () => { CALL { WITH this0 OPTIONAL MATCH (this0_posts_connect0_node:Post) - OPTIONAL MATCH (this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_0_0_0_before_this0:User) - WITH *, count(authorization_0_0_0_0_before_this0) AS creatorCount + OPTIONAL MATCH (this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_before_this0:User) + WITH *, count(authorization_0_before_this0) AS creatorCount WITH * - WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_0_0_0_0_before_this0.id = $jwt.sub))) + WHERE ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_0_before_this0.id = $jwt.sub))) CALL { WITH * WITH collect(this0_posts_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -892,10 +902,10 @@ describe("Cypher Auth Where", () => { CALL { WITH this0 OPTIONAL MATCH (this0_posts_connect0_node:Post) - OPTIONAL MATCH (this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_0_0_0_before_this0:User) - WITH *, count(authorization_0_0_0_0_before_this0) AS creatorCount + OPTIONAL MATCH (this0_posts_connect0_node)<-[:HAS_POST]-(authorization_0_before_this0:User) + WITH *, count(authorization_0_before_this0) AS creatorCount WITH * - WHERE this0_posts_connect0_node.id = $this0_posts_connect0_node_param0 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_0_0_0_0_before_this0.id = $jwt.sub))) + WHERE this0_posts_connect0_node.id = $this0_posts_connect0_node_param0 AND ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_0_before_this0.id = $jwt.sub))) CALL { WITH * WITH collect(this0_posts_connect0_node) as connectedNodes, collect(this0) as parentNodes diff --git a/packages/graphql/tests/tck/directives/authorization/projection-connection-union.test.ts b/packages/graphql/tests/tck/directives/authorization/projection-connection-union.test.ts index 872011ef23e..aee46636174 100644 --- a/packages/graphql/tests/tck/directives/authorization/projection-connection-union.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/projection-connection-union.test.ts @@ -104,19 +104,24 @@ describe("Cypher Auth Projection On Connections On Unions", () => { WITH this1 MATCH (this1)<-[this3:HAS_POST]-(this4:User) WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { name: this4.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this4, relationship: this3 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var5 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this4, edge.relationship AS this3 + RETURN collect({ node: { name: this4.name } }) AS var5 + } + RETURN { edges: var5, totalCount: totalCount } AS var6 } - WITH { node: { __resolveType: \\"Post\\", __id: id(this1), content: this1.content, creatorConnection: var5 } } AS edge + WITH { node: { __resolveType: \\"Post\\", __id: id(this1), content: this1.content, creatorConnection: var6 } } AS edge RETURN edge } WITH collect(edge) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var6 + RETURN { edges: edges, totalCount: totalCount } AS var7 } - RETURN this { contentConnection: var6 } AS this" + RETURN this { contentConnection: var7 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/directives/authorization/projection-connection.test.ts b/packages/graphql/tests/tck/directives/authorization/projection-connection.test.ts index d27c70487c5..3b80fbb2032 100644 --- a/packages/graphql/tests/tck/directives/authorization/projection-connection.test.ts +++ b/packages/graphql/tests/tck/directives/authorization/projection-connection.test.ts @@ -88,12 +88,17 @@ describe("Cypher Auth Projection On Connections", () => { WITH *, count(this2) AS creatorCount WITH * WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { content: this1.content } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { content: this1.content } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 } - RETURN this { .name, postsConnection: var3 } AS this" + RETURN this { .name, postsConnection: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -146,21 +151,31 @@ describe("Cypher Auth Projection On Connections", () => { WITH *, count(this2) AS creatorCount WITH * WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH collect({ node: this1, relationship: this0 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this1 - MATCH (this1)<-[this3:HAS_POST]-(this4:User) - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { name: this4.name } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var5 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + CALL { + WITH this1 + MATCH (this1)<-[this3:HAS_POST]-(this4:User) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH collect({ node: this4, relationship: this3 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this4, edge.relationship AS this3 + RETURN collect({ node: { name: this4.name } }) AS var5 + } + RETURN { edges: var5, totalCount: totalCount } AS var6 + } + RETURN collect({ node: { content: this1.content, creatorConnection: var6 } }) AS var7 } - WITH { node: { content: this1.content, creatorConnection: var5 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var6 + RETURN { edges: var7, totalCount: totalCount } AS var8 } - RETURN this { .name, postsConnection: var6 } AS this" + RETURN this { .name, postsConnection: var8 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -234,27 +249,34 @@ describe("Cypher Auth Projection On top-level connections", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this - MATCH (this)-[this0:HAS_POST]->(this1:Post) - OPTIONAL MATCH (this1)<-[:HAS_POST]-(this2:User) - WITH *, count(this2) AS creatorCount - WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { content: this1.content } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + MATCH (this0)-[this1:HAS_POST]->(this2:Post) + OPTIONAL MATCH (this2)<-[:HAS_POST]-(this3:User) + WITH *, count(this3) AS creatorCount + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { content: this2.content } }) AS var4 + } + RETURN { edges: var4, totalCount: totalCount } AS var5 + } + RETURN collect({ node: { name: this0.name, postsConnection: var5 } }) AS var6 } - WITH { node: this { .name, postsConnection: var3 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var6, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -301,36 +323,48 @@ describe("Cypher Auth Projection On top-level connections", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:User) - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH collect(this) AS edges + "MATCH (this0:User) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this - MATCH (this)-[this0:HAS_POST]->(this1:Post) - OPTIONAL MATCH (this1)<-[:HAS_POST]-(this2:User) - WITH *, count(this2) AS creatorCount - WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 CALL { - WITH this1 - MATCH (this1)<-[this3:HAS_POST]-(this4:User) - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this4.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { name: this4.name } } AS edge - WITH collect(edge) AS edges + WITH this0 + MATCH (this0)-[this1:HAS_POST]->(this2:Post) + OPTIONAL MATCH (this2)<-[:HAS_POST]-(this3:User) + WITH *, count(this3) AS creatorCount + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH collect({ node: this2, relationship: this1 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var5 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + CALL { + WITH this2 + MATCH (this2)<-[this4:HAS_POST]-(this5:User) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.sub IS NOT NULL AND this5.id = $jwt.sub)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH collect({ node: this5, relationship: this4 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this5, edge.relationship AS this4 + RETURN collect({ node: { name: this5.name } }) AS var6 + } + RETURN { edges: var6, totalCount: totalCount } AS var7 + } + RETURN collect({ node: { content: this2.content, creatorConnection: var7 } }) AS var8 + } + RETURN { edges: var8, totalCount: totalCount } AS var9 } - WITH { node: { content: this1.content, creatorConnection: var5 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var6 + RETURN collect({ node: { name: this0.name, postsConnection: var9 } }) AS var10 } - WITH { node: this { .name, postsConnection: var6 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var10, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/directives/coalesce.test.ts b/packages/graphql/tests/tck/directives/coalesce.test.ts index ae9cb545605..6bd0cf81f42 100644 --- a/packages/graphql/tests/tck/directives/coalesce.test.ts +++ b/packages/graphql/tests/tck/directives/coalesce.test.ts @@ -215,12 +215,17 @@ describe("Cypher coalesce()", () => { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Movie) WHERE coalesce(this1.status, \\"ACTIVE\\") = $param0 - WITH { node: { id: this1.id, status: this1.status } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { id: this1.id, status: this1.status } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { moviesConnection: var2 } AS this" + RETURN this { moviesConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -273,12 +278,17 @@ describe("Cypher coalesce()", () => { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Movie) WHERE coalesce(this1.statuses, [\\"ACTIVE\\", \\"INACTIVE\\"]) = $param0 - WITH { node: { id: this1.id, statuses: this1.statuses } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { id: this1.id, statuses: this1.statuses } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { moviesConnection: var2 } AS this" + RETURN this { moviesConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/directives/node/node-label.test.ts b/packages/graphql/tests/tck/directives/node/node-label.test.ts index 2170d099177..3831d73378d 100644 --- a/packages/graphql/tests/tck/directives/node/node-label.test.ts +++ b/packages/graphql/tests/tck/directives/node/node-label.test.ts @@ -115,12 +115,17 @@ describe("Label in Node directive", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Person) - WITH { node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); diff --git a/packages/graphql/tests/tck/directives/node/node-with-auth-projection.test.ts b/packages/graphql/tests/tck/directives/node/node-with-auth-projection.test.ts index 2785c72b60d..973369ee3df 100644 --- a/packages/graphql/tests/tck/directives/node/node-with-auth-projection.test.ts +++ b/packages/graphql/tests/tck/directives/node/node-with-auth-projection.test.ts @@ -84,12 +84,17 @@ describe("Cypher Auth Projection On Connections", () => { WITH *, count(this2) AS creatorCount WITH * WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (creatorCount <> 0 AND ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { content: this1.content } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { content: this1.content } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 } - RETURN this { .name, postsConnection: var3 } AS this" + RETURN this { .name, postsConnection: var4 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/experimental/aggregations/filter-aggregation-interfaces-field-level.test.ts b/packages/graphql/tests/tck/experimental/aggregations/filter-aggregation-interfaces-field-level.test.ts index a541177ae35..08788c3ba1d 100644 --- a/packages/graphql/tests/tck/experimental/aggregations/filter-aggregation-interfaces-field-level.test.ts +++ b/packages/graphql/tests/tck/experimental/aggregations/filter-aggregation-interfaces-field-level.test.ts @@ -189,4 +189,95 @@ describe("Interface Field Level Aggregations", () => { }" `); }); + + test("Count with OR operator and string aggregation", async () => { + const query = gql` + { + actors { + actedInAggregate(where: { OR: [{ title_STARTS_WITH: "The" }, { title_STARTS_WITH: "A" }] }) { + count + edge { + screenTime { + min + max + } + } + node { + title { + longest + shortest + } + } + } + name + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + CALL { + WITH this + CALL { + WITH this + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + RETURN this1 AS node, this0 AS edge + UNION + WITH this + MATCH (this)-[this2:ACTED_IN]->(this3:Series) + RETURN this3 AS node, this2 AS edge + } + WITH * + WHERE (node.title STARTS WITH $param0 OR node.title STARTS WITH $param1) + RETURN count(node) AS this4 + } + CALL { + WITH this + CALL { + WITH this + MATCH (this)-[this5:ACTED_IN]->(this6:Movie) + RETURN this6 AS node, this5 AS edge + UNION + WITH this + MATCH (this)-[this7:ACTED_IN]->(this8:Series) + RETURN this8 AS node, this7 AS edge + } + WITH * + WHERE (node.title STARTS WITH $param2 OR node.title STARTS WITH $param3) + WITH node + ORDER BY size(node.title) DESC + WITH collect(node.title) AS list + RETURN { longest: head(list), shortest: last(list) } AS this9 + } + CALL { + WITH this + CALL { + WITH this + MATCH (this)-[this10:ACTED_IN]->(this11:Movie) + RETURN this11 AS node, this10 AS edge + UNION + WITH this + MATCH (this)-[this12:ACTED_IN]->(this13:Series) + RETURN this13 AS node, this12 AS edge + } + WITH * + WHERE (node.title STARTS WITH $param4 OR node.title STARTS WITH $param5) + RETURN { min: min(edge.screenTime), max: max(edge.screenTime) } AS this14 + } + RETURN this { .name, actedInAggregate: { count: this4, node: { title: this9 }, edge: { screenTime: this14 } } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The\\", + \\"param1\\": \\"A\\", + \\"param2\\": \\"The\\", + \\"param3\\": \\"A\\", + \\"param4\\": \\"The\\", + \\"param5\\": \\"A\\" + }" + `); + }); }); diff --git a/packages/graphql/tests/tck/experimental/aggregations/filter-aggregation-interfaces-top-level.test.ts b/packages/graphql/tests/tck/experimental/aggregations/filter-aggregation-interfaces-top-level.test.ts index 94f7b43f175..e2c0a6dd81e 100644 --- a/packages/graphql/tests/tck/experimental/aggregations/filter-aggregation-interfaces-top-level.test.ts +++ b/packages/graphql/tests/tck/experimental/aggregations/filter-aggregation-interfaces-top-level.test.ts @@ -128,4 +128,74 @@ describe("Top level filter on aggregation interfaces", () => { }" `); }); + + test("top level count and string fields with AND operation", async () => { + const query = gql` + { + productionsAggregate(where: { AND: [{ cost_GTE: 10 }, { title: "The Matrix" }] }) { + count + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CALL { + MATCH (this0:Movie) + RETURN this0 AS node + UNION + MATCH (this1:Series) + RETURN this1 AS node + } + WITH * + WHERE (node.cost >= $param0 AND node.title = $param1) + RETURN count(node) AS this2 + } + RETURN { count: this2 }" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": 10, + \\"param1\\": \\"The Matrix\\" + }" + `); + }); + + test("top level count and string fields with OR operation", async () => { + const query = gql` + { + productionsAggregate(where: { OR: [{ cost_GTE: 10 }, { title: "The Matrix" }] }) { + count + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CALL { + MATCH (this0:Movie) + RETURN this0 AS node + UNION + MATCH (this1:Series) + RETURN this1 AS node + } + WITH * + WHERE (node.cost >= $param0 OR node.title = $param1) + RETURN count(node) AS this2 + } + RETURN { count: this2 }" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": 10, + \\"param1\\": \\"The Matrix\\" + }" + `); + }); }); diff --git a/packages/graphql/tests/tck/experimental/interface-authorization.test.ts b/packages/graphql/tests/tck/experimental/interface-authorization.test.ts index ce22012a6d5..e439d80308f 100644 --- a/packages/graphql/tests/tck/experimental/interface-authorization.test.ts +++ b/packages/graphql/tests/tck/experimental/interface-authorization.test.ts @@ -17,11 +17,11 @@ * limitations under the License. */ -import { gql } from "graphql-tag"; import type { DocumentNode } from "graphql"; +import { gql } from "graphql-tag"; import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; import { createBearerToken } from "../../utils/create-bearer-token"; +import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; describe("Interface top level operations with authorization", () => { const secret = "secret"; @@ -106,7 +106,8 @@ describe("Interface top level operations with authorization", () => { WITH this2 { .id, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 RETURN this2 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -153,7 +154,8 @@ describe("Interface top level operations with authorization", () => { WITH this2 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 RETURN this2 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -203,7 +205,8 @@ describe("Interface top level operations with authorization", () => { WITH this2 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 RETURN this2 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -255,7 +258,8 @@ describe("Interface top level operations with authorization", () => { WITH this2 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 RETURN this2 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -302,7 +306,8 @@ describe("Interface top level operations with authorization", () => { WITH this0 { .id, other: var3, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 RETURN this0 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -336,7 +341,8 @@ describe("Interface top level operations with authorization", () => { WITH this0 { .id, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 RETURN this0 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -374,7 +380,8 @@ describe("Interface top level operations with authorization", () => { WITH this0 { .id, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 RETURN this0 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -389,191 +396,4 @@ describe("Interface top level operations with authorization", () => { }" `); }); - - test("Read interface with filters", async () => { - const query = gql` - { - myOtherInterfaces(where: { _on: { SomeNodeType: { other: { id: "2" } } } }) { - id - ... on SomeNodeType { - id - other { - id - } - } - } - } - `; - - const token = createBearerToken("secret", { jwtAllowedNamesExample: "Horror" }); - const result = await translateQuery(neoSchema, query, { token }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:SomeNodeType) - OPTIONAL MATCH (this0)-[:HAS_OTHER_NODES]->(this1:OtherNodeType) - WITH *, count(this1) AS otherCount - WITH * - WHERE ((otherCount <> 0 AND this1.id = $param0) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.jwtAllowedNamesExample IS NOT NULL AND this0.id = $jwt.jwtAllowedNamesExample) AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - CALL { - WITH this0 - MATCH (this0)-[this2:HAS_OTHER_NODES]->(this3:OtherNodeType) - WITH this3 { .id } AS this3 - RETURN head(collect(this3)) AS var4 - } - WITH this0 { .id, other: var4, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 - RETURN this0 AS this - } - RETURN this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"2\\", - \\"isAuthenticated\\": true, - \\"jwt\\": { - \\"roles\\": [], - \\"jwtAllowedNamesExample\\": \\"Horror\\" - }, - \\"param3\\": \\"admin\\" - }" - `); - }); - - test("Read interface with filters (interface target of a relationship)", async () => { - const query = gql` - { - myInterfaces( - where: { - _on: { - SomeNodeType: { somethingElse_NOT: "test" } - MyOtherImplementationType: { someField: "bla" } - } - } - ) { - id - ... on MyOtherImplementationType { - someField - } - ... on MyOtherInterface { - something - ... on SomeNodeType { - somethingElse - } - } - } - } - `; - - const token = createBearerToken("secret", { jwtAllowedNamesExample: "Horror" }); - const result = await translateQuery(neoSchema, query, { token }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:SomeNodeType) - WHERE (NOT (this0.somethingElse = $param0) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.jwtAllowedNamesExample IS NOT NULL AND this0.id = $jwt.jwtAllowedNamesExample) AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - WITH this0 { .id, .something, .somethingElse, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 - RETURN this0 AS this - UNION - MATCH (this1:MyOtherImplementationType) - WHERE this1.someField = $param4 - WITH this1 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this1) } AS this1 - RETURN this1 AS this - } - RETURN this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"test\\", - \\"isAuthenticated\\": true, - \\"jwt\\": { - \\"roles\\": [], - \\"jwtAllowedNamesExample\\": \\"Horror\\" - }, - \\"param3\\": \\"admin\\", - \\"param4\\": \\"bla\\" - }" - `); - }); - - test("Type filtering using onType", async () => { - const query = gql` - { - myInterfaces(where: { _on: { MyOtherImplementationType: {} } }) { - id - ... on MyOtherImplementationType { - someField - } - } - } - `; - - const token = createBearerToken("secret", { jwtAllowedNamesExample: "Horror" }); - const result = await translateQuery(neoSchema, query, { token }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:MyOtherImplementationType) - WITH this0 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this0) } AS this0 - RETURN this0 AS this - } - RETURN this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("Filter overriding using onType", async () => { - const query = gql` - { - myInterfaces( - where: { id_STARTS_WITH: "4", _on: { MyOtherImplementationType: { id_STARTS_WITH: "1" } } } - ) { - id - ... on MyOtherImplementationType { - someField - } - } - } - `; - - const token = createBearerToken("secret", { jwtAllowedNamesExample: "Horror" }); - const result = await translateQuery(neoSchema, query, { token }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:SomeNodeType) - WHERE (this0.id STARTS WITH $param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.jwtAllowedNamesExample IS NOT NULL AND this0.id = $jwt.jwtAllowedNamesExample) AND ($jwt.roles IS NOT NULL AND $param3 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - WITH this0 { .id, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 - RETURN this0 AS this - UNION - MATCH (this1:MyImplementationType) - WHERE (this1.id STARTS WITH $param4 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.groups IS NOT NULL AND $param5 IN $jwt.groups)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - WITH this1 { .id, __resolveType: \\"MyImplementationType\\", __id: id(this1) } AS this1 - RETURN this1 AS this - UNION - MATCH (this2:MyOtherImplementationType) - WHERE this2.id STARTS WITH $param6 - WITH this2 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 - RETURN this2 AS this - } - RETURN this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"4\\", - \\"isAuthenticated\\": true, - \\"jwt\\": { - \\"roles\\": [], - \\"jwtAllowedNamesExample\\": \\"Horror\\" - }, - \\"param3\\": \\"admin\\", - \\"param4\\": \\"4\\", - \\"param5\\": \\"a\\", - \\"param6\\": \\"1\\" - }" - `); - }); }); diff --git a/packages/graphql/tests/tck/experimental/interface-filtering.test.ts b/packages/graphql/tests/tck/experimental/interface-filtering.test.ts new file mode 100644 index 00000000000..76bb8dd16b9 --- /dev/null +++ b/packages/graphql/tests/tck/experimental/interface-filtering.test.ts @@ -0,0 +1,152 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DocumentNode } from "graphql"; +import { gql } from "graphql-tag"; +import { Neo4jGraphQL } from "../../../src"; +import { createBearerToken } from "../../utils/create-bearer-token"; +import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; + +describe("Interface filtering operations", () => { + const secret = "secret"; + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeEach(() => { + typeDefs = gql` + interface Show { + title: String! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Movie implements Show @limit(default: 3, max: 10) { + title: String! + cost: Float + runtime: Int + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Series implements Show { + title: String! + episodes: Int + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Actor { + name: String! + actedIn: [Show!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + interface ActedIn @relationshipProperties { + screenTime: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + features: { authorization: { key: secret } }, + experimental: true, + }); + }); + + test("Logical operator filter (top level)", async () => { + const query = gql` + query actedInWhere { + shows(where: { OR: [{ title: "The Office" }, { title: "The Office 2" }] }) { + title + } + } + `; + + const token = createBearerToken(secret); + const result = await translateQuery(neoSchema, query, { token }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + MATCH (this0:Movie) + WHERE (this0.title = $param0 OR this0.title = $param1) + WITH this0 { .title, __resolveType: \\"Movie\\", __id: id(this0) } AS this0 + RETURN this0 AS this + UNION + MATCH (this1:Series) + WHERE (this1.title = $param2 OR this1.title = $param3) + WITH this1 { .title, __resolveType: \\"Series\\", __id: id(this1) } AS this1 + RETURN this1 AS this + } + WITH this + RETURN this AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Office\\", + \\"param1\\": \\"The Office 2\\", + \\"param2\\": \\"The Office\\", + \\"param3\\": \\"The Office 2\\" + }" + `); + }); + + test("Logical operator filter on relationship", async () => { + const query = gql` + query actedInWhere { + actors { + actedIn(where: { OR: [{ title: "The Office" }, { title: "The Office 2" }] }) { + title + } + } + } + `; + + const token = createBearerToken(secret); + const result = await translateQuery(neoSchema, query, { token }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + CALL { + WITH this + CALL { + WITH * + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WHERE (this1.title = $param0 OR this1.title = $param1) + WITH this1 { .title, __resolveType: \\"Movie\\", __id: id(this1) } AS this1 + RETURN this1 AS var2 + UNION + WITH * + MATCH (this)-[this3:ACTED_IN]->(this4:Series) + WHERE (this4.title = $param2 OR this4.title = $param3) + WITH this4 { .title, __resolveType: \\"Series\\", __id: id(this4) } AS this4 + RETURN this4 AS var2 + } + WITH var2 + RETURN collect(var2) AS var2 + } + RETURN this { actedIn: var2 } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Office\\", + \\"param1\\": \\"The Office 2\\", + \\"param2\\": \\"The Office\\", + \\"param3\\": \\"The Office 2\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/experimental/interface-pagination.test.ts b/packages/graphql/tests/tck/experimental/interface-pagination.test.ts index 3a442e4b0fe..ebd06368076 100644 --- a/packages/graphql/tests/tck/experimental/interface-pagination.test.ts +++ b/packages/graphql/tests/tck/experimental/interface-pagination.test.ts @@ -19,7 +19,7 @@ import { gql } from "graphql-tag"; import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; +import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; describe("Top-level Interface query pagination (sort and limit)", () => { let typeDefs: string; @@ -104,9 +104,10 @@ describe("Top-level Interface query pagination (sort and limit)", () => { WITH this5 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this5) } AS this5 RETURN this5 AS this } - RETURN this + WITH this ORDER BY this.id ASC - LIMIT $param0" + LIMIT $param0 + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -149,115 +150,10 @@ describe("Top-level Interface query pagination (sort and limit)", () => { WITH this2 { .someField, .id, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 RETURN this2 AS this } - RETURN this + WITH this ORDER BY this.id ASC - LIMIT $param0" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": { - \\"low\\": 10, - \\"high\\": 0 - } - }" - `); - }); - - test("Sort with filter on Interface top-level", async () => { - const query = gql` - query { - myInterfaces( - where: { _on: { SomeNodeType: { somethingElse_NOT: "test" }, MyOtherImplementationType: {} } } - options: { sort: [{ id: ASC }], limit: 10 } - ) { - id - ... on MyOtherImplementationType { - someField - } - ... on MyOtherInterface { - something - ... on SomeNodeType { - somethingElse - other { - id - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:SomeNodeType) - WHERE NOT (this0.somethingElse = $param0) - CALL { - WITH this0 - MATCH (this0)-[this1:HAS_OTHER_NODES]->(this2:OtherNodeType) - WITH this2 { .id } AS this2 - RETURN collect(this2) AS var3 - } - WITH this0 { .id, .something, .somethingElse, other: var3, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 - RETURN this0 AS this - UNION - MATCH (this4:MyOtherImplementationType) - WITH this4 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this4) } AS this4 - RETURN this4 AS this - } - RETURN this - ORDER BY this.id ASC - LIMIT $param1" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"test\\", - \\"param1\\": { - \\"low\\": 10, - \\"high\\": 0 - } - }" - `); - }); - - test("Sort on Interfaces filtered by _on type", async () => { - const query = gql` - query { - myInterfaces( - where: { _on: { MyOtherImplementationType: {} } } - options: { sort: [{ id: ASC }], limit: 10 } - ) { - id - ... on MyOtherImplementationType { - someField - } - ... on MyOtherInterface { - something - ... on SomeNodeType { - somethingElse - other { - id - } - } - } - } - } - `; - - const result = await translateQuery(neoSchema, query); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:MyOtherImplementationType) - WITH this0 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this0) } AS this0 - RETURN this0 AS this - } - RETURN this - ORDER BY this.id ASC - LIMIT $param0" + LIMIT $param0 + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -315,9 +211,10 @@ describe("Top-level Interface query pagination (sort and limit)", () => { WITH this5 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this5) } AS this5 RETURN this5 AS this } - RETURN this + WITH this ORDER BY this.id ASC - LIMIT $param1" + LIMIT $param1 + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -446,8 +343,9 @@ describe("Top-level Interface query pagination (sort and limit)", () => { WITH this5 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this5) } AS this5 RETURN this5 AS this } - RETURN this - LIMIT $param0" + WITH this + LIMIT $param0 + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -502,8 +400,9 @@ describe("Top-level Interface query pagination (sort and limit)", () => { WITH this5 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this5) } AS this5 RETURN this5 AS this } - RETURN this - LIMIT $param0" + WITH this + LIMIT $param0 + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -558,8 +457,9 @@ describe("Top-level Interface query pagination (sort and limit)", () => { WITH this5 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this5) } AS this5 RETURN this5 AS this } - RETURN this - LIMIT $param0" + WITH this + LIMIT $param0 + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -614,9 +514,10 @@ describe("Top-level Interface query pagination (sort and limit)", () => { WITH this5 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this5) } AS this5 RETURN this5 AS this } - RETURN this + WITH this ORDER BY this.id ASC - LIMIT $param0" + LIMIT $param0 + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/experimental/interface-top-level.test.ts b/packages/graphql/tests/tck/experimental/interface-top-level.test.ts index d40f2e68337..62f9120a011 100644 --- a/packages/graphql/tests/tck/experimental/interface-top-level.test.ts +++ b/packages/graphql/tests/tck/experimental/interface-top-level.test.ts @@ -17,11 +17,11 @@ * limitations under the License. */ -import { gql } from "graphql-tag"; import type { DocumentNode } from "graphql"; +import { gql } from "graphql-tag"; import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; import { createBearerToken } from "../../utils/create-bearer-token"; +import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; describe("Interface top level operations", () => { const secret = "secret"; @@ -90,7 +90,8 @@ describe("Interface top level operations", () => { WITH this2 { .id, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 RETURN this2 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -125,7 +126,8 @@ describe("Interface top level operations", () => { WITH this2 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 RETURN this2 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -163,7 +165,8 @@ describe("Interface top level operations", () => { WITH this2 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 RETURN this2 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -203,7 +206,8 @@ describe("Interface top level operations", () => { WITH this2 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 RETURN this2 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -239,7 +243,8 @@ describe("Interface top level operations", () => { WITH this0 { .id, other: var3, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 RETURN this0 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -264,7 +269,8 @@ describe("Interface top level operations", () => { WITH this0 { .id, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 RETURN this0 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -296,7 +302,8 @@ describe("Interface top level operations", () => { WITH this0 { .id, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 RETURN this0 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -305,172 +312,4 @@ describe("Interface top level operations", () => { }" `); }); - - test("Read interface with filters", async () => { - const query = gql` - { - myOtherInterfaces(where: { _on: { SomeNodeType: { other: { id: "2" } } } }) { - id - ... on SomeNodeType { - id - other { - id - } - } - } - } - `; - - const token = createBearerToken("secret", { jwtAllowedNamesExample: "Horror" }); - const result = await translateQuery(neoSchema, query, { token }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:SomeNodeType) - OPTIONAL MATCH (this0)-[:HAS_OTHER_NODES]->(this1:OtherNodeType) - WITH *, count(this1) AS otherCount - WITH * - WHERE (otherCount <> 0 AND this1.id = $param0) - CALL { - WITH this0 - MATCH (this0)-[this2:HAS_OTHER_NODES]->(this3:OtherNodeType) - WITH this3 { .id } AS this3 - RETURN head(collect(this3)) AS var4 - } - WITH this0 { .id, other: var4, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 - RETURN this0 AS this - } - RETURN this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"2\\" - }" - `); - }); - - test("Read interface with filters (interface target of a relationship)", async () => { - const query = gql` - { - myInterfaces( - where: { - _on: { - SomeNodeType: { somethingElse_NOT: "test" } - MyOtherImplementationType: { someField: "bla" } - } - } - ) { - id - ... on MyOtherImplementationType { - someField - } - ... on MyOtherInterface { - something - ... on SomeNodeType { - somethingElse - } - } - } - } - `; - - const token = createBearerToken("secret", { jwtAllowedNamesExample: "Horror" }); - const result = await translateQuery(neoSchema, query, { token }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:SomeNodeType) - WHERE NOT (this0.somethingElse = $param0) - WITH this0 { .id, .something, .somethingElse, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 - RETURN this0 AS this - UNION - MATCH (this1:MyOtherImplementationType) - WHERE this1.someField = $param1 - WITH this1 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this1) } AS this1 - RETURN this1 AS this - } - RETURN this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"test\\", - \\"param1\\": \\"bla\\" - }" - `); - }); - - test("Type filtering using onType", async () => { - const query = gql` - { - myInterfaces(where: { _on: { MyOtherImplementationType: {} } }) { - id - ... on MyOtherImplementationType { - someField - } - } - } - `; - - const token = createBearerToken("secret", { jwtAllowedNamesExample: "Horror" }); - const result = await translateQuery(neoSchema, query, { token }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:MyOtherImplementationType) - WITH this0 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this0) } AS this0 - RETURN this0 AS this - } - RETURN this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); - }); - - test("Filter overriding using onType", async () => { - const query = gql` - { - myInterfaces( - where: { id_STARTS_WITH: "4", _on: { MyOtherImplementationType: { id_STARTS_WITH: "1" } } } - ) { - id - ... on MyOtherImplementationType { - someField - } - } - } - `; - - const token = createBearerToken("secret", { jwtAllowedNamesExample: "Horror" }); - const result = await translateQuery(neoSchema, query, { token }); - - expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:SomeNodeType) - WHERE this0.id STARTS WITH $param0 - WITH this0 { .id, __resolveType: \\"SomeNodeType\\", __id: id(this0) } AS this0 - RETURN this0 AS this - UNION - MATCH (this1:MyImplementationType) - WHERE this1.id STARTS WITH $param1 - WITH this1 { .id, __resolveType: \\"MyImplementationType\\", __id: id(this1) } AS this1 - RETURN this1 AS this - UNION - MATCH (this2:MyOtherImplementationType) - WHERE this2.id STARTS WITH $param2 - WITH this2 { .id, .someField, __resolveType: \\"MyOtherImplementationType\\", __id: id(this2) } AS this2 - RETURN this2 AS this - } - RETURN this" - `); - - expect(formatParams(result.params)).toMatchInlineSnapshot(` - "{ - \\"param0\\": \\"4\\", - \\"param1\\": \\"4\\", - \\"param2\\": \\"1\\" - }" - `); - }); }); diff --git a/packages/graphql/tests/tck/experimental/typename-in-auth.test.ts b/packages/graphql/tests/tck/experimental/typename-in-auth.test.ts new file mode 100644 index 00000000000..3e7c2ce1603 --- /dev/null +++ b/packages/graphql/tests/tck/experimental/typename-in-auth.test.ts @@ -0,0 +1,338 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gql } from "graphql-tag"; +import { Neo4jGraphQL } from "../../../src"; +import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; + +describe("typename_IN with auth", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + interface Production { + title: String! + cost: Float! + } + + type Movie implements Production { + title: String! + cost: Float! + runtime: Int! + } + + type Series implements Production { + title: String! + cost: Float! + episodes: Int! + } + + type Cartoon implements Production { + title: String! + cost: Float! + cartoonist: String! + } + type Actor { + name: String! + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + interface ActedIn @relationshipProperties { + screenTime: Int! + } + `; + }); + + describe("validate", () => { + beforeAll(() => { + const authTypeDefs = + typeDefs + + /* GraphQL */ ` + extend type Actor + @authorization( + validate: [ + { + when: [BEFORE] + operations: [READ] + where: { + node: { + actedInConnection_SOME: { + node: { title: "The Matrix", typename_IN: [Series] } + } + } + } + } + ] + ) + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs: authTypeDefs, + experimental: true, + }); + }); + + test("read", async () => { + const query = gql` + { + actors { + actedIn { + title + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this)-[this1:ACTED_IN]->(this0) WHERE ((($param1 IS NOT NULL AND this0.title = $param1) AND this0:Series) AND (this0:Movie OR this0:Series OR this0:Cartoon)) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this + CALL { + WITH * + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH this3 { .title, __resolveType: \\"Movie\\", __id: id(this3) } AS this3 + RETURN this3 AS var4 + UNION + WITH * + MATCH (this)-[this5:ACTED_IN]->(this6:Series) + WITH this6 { .title, __resolveType: \\"Series\\", __id: id(this6) } AS this6 + RETURN this6 AS var4 + UNION + WITH * + MATCH (this)-[this7:ACTED_IN]->(this8:Cartoon) + WITH this8 { .title, __resolveType: \\"Cartoon\\", __id: id(this8) } AS this8 + RETURN this8 AS var4 + } + WITH var4 + RETURN collect(var4) AS var4 + } + RETURN this { actedIn: var4 } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"isAuthenticated\\": false, + \\"param1\\": \\"The Matrix\\" + }" + `); + }); + + test("connection read", async () => { + const query = gql` + { + actorsConnection { + edges { + node { + actedIn { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Actor) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)-[this2:ACTED_IN]->(this1) WHERE ((($param1 IS NOT NULL AND this1.title = $param1) AND this1:Series) AND (this1:Movie OR this1:Series OR this1:Cartoon)) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + CALL { + WITH * + MATCH (this0)-[this3:ACTED_IN]->(this4:Movie) + WITH this4 { .title, __resolveType: \\"Movie\\", __id: id(this4) } AS this4 + RETURN this4 AS var5 + UNION + WITH * + MATCH (this0)-[this6:ACTED_IN]->(this7:Series) + WITH this7 { .title, __resolveType: \\"Series\\", __id: id(this7) } AS this7 + RETURN this7 AS var5 + UNION + WITH * + MATCH (this0)-[this8:ACTED_IN]->(this9:Cartoon) + WITH this9 { .title, __resolveType: \\"Cartoon\\", __id: id(this9) } AS this9 + RETURN this9 AS var5 + } + WITH var5 + RETURN collect(var5) AS var5 + } + RETURN collect({ node: { actedIn: var5 } }) AS var10 + } + RETURN { edges: var10, totalCount: totalCount } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"isAuthenticated\\": false, + \\"param1\\": \\"The Matrix\\" + }" + `); + }); + }); + + describe("filter", () => { + beforeAll(() => { + const authTypeDefs = + typeDefs + + /* GraphQL */ ` + extend type Actor + @authorization( + filter: [ + { + where: { + node: { + actedInConnection_SOME: { + node: { title: "The Matrix", typename_IN: [Series] } + } + } + } + } + ] + ) + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs: authTypeDefs, + experimental: true, + }); + }); + + test("read", async () => { + const query = gql` + { + actors { + actedIn { + title + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + WITH * + WHERE ($isAuthenticated = true AND size([(this)-[this1:ACTED_IN]->(this0) WHERE ((($param1 IS NOT NULL AND this0.title = $param1) AND this0:Series) AND (this0:Movie OR this0:Series OR this0:Cartoon)) | 1]) > 0) + CALL { + WITH this + CALL { + WITH * + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH this3 { .title, __resolveType: \\"Movie\\", __id: id(this3) } AS this3 + RETURN this3 AS var4 + UNION + WITH * + MATCH (this)-[this5:ACTED_IN]->(this6:Series) + WITH this6 { .title, __resolveType: \\"Series\\", __id: id(this6) } AS this6 + RETURN this6 AS var4 + UNION + WITH * + MATCH (this)-[this7:ACTED_IN]->(this8:Cartoon) + WITH this8 { .title, __resolveType: \\"Cartoon\\", __id: id(this8) } AS this8 + RETURN this8 AS var4 + } + WITH var4 + RETURN collect(var4) AS var4 + } + RETURN this { actedIn: var4 } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"isAuthenticated\\": false, + \\"param1\\": \\"The Matrix\\" + }" + `); + }); + + test("connection read", async () => { + const query = gql` + { + actorsConnection { + edges { + node { + actedIn { + title + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Actor) + WHERE ($isAuthenticated = true AND size([(this0)-[this2:ACTED_IN]->(this1) WHERE ((($param1 IS NOT NULL AND this1.title = $param1) AND this1:Series) AND (this1:Movie OR this1:Series OR this1:Cartoon)) | 1]) > 0) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + CALL { + WITH * + MATCH (this0)-[this3:ACTED_IN]->(this4:Movie) + WITH this4 { .title, __resolveType: \\"Movie\\", __id: id(this4) } AS this4 + RETURN this4 AS var5 + UNION + WITH * + MATCH (this0)-[this6:ACTED_IN]->(this7:Series) + WITH this7 { .title, __resolveType: \\"Series\\", __id: id(this7) } AS this7 + RETURN this7 AS var5 + UNION + WITH * + MATCH (this0)-[this8:ACTED_IN]->(this9:Cartoon) + WITH this9 { .title, __resolveType: \\"Cartoon\\", __id: id(this9) } AS this9 + RETURN this9 AS var5 + } + WITH var5 + RETURN collect(var5) AS var5 + } + RETURN collect({ node: { actedIn: var5 } }) AS var10 + } + RETURN { edges: var10, totalCount: totalCount } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"isAuthenticated\\": false, + \\"param1\\": \\"The Matrix\\" + }" + `); + }); + }); +}); diff --git a/packages/graphql/tests/tck/experimental/typename-in.test.ts b/packages/graphql/tests/tck/experimental/typename-in.test.ts new file mode 100644 index 00000000000..03753f624fc --- /dev/null +++ b/packages/graphql/tests/tck/experimental/typename-in.test.ts @@ -0,0 +1,311 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gql } from "graphql-tag"; +import { Neo4jGraphQL } from "../../../src"; +import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; + +describe("typename_IN", () => { + let typeDefs: string; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = /* GraphQL */ ` + interface Production { + title: String! + cost: Float! + } + + type Movie implements Production { + title: String! + cost: Float! + runtime: Int! + } + + type Series implements Production { + title: String! + cost: Float! + episodes: Int! + } + + type Cartoon implements Production { + title: String! + cost: Float! + cartoonist: String! + } + + interface ActedIn @relationshipProperties { + screenTime: Int! + } + + type Actor { + name: String! + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + experimental: true, + }); + }); + + test("top-level", async () => { + const query = gql` + { + productions( + where: { + OR: [{ AND: [{ title: "The Matrix" }, { typename_IN: [Series] }] }, { typename_IN: [Movie] }] + } + ) { + title + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + MATCH (this0:Movie) + WHERE ((this0.title = $param0 AND this0:Series) OR this0:Movie) + WITH this0 { .title, __resolveType: \\"Movie\\", __id: id(this0) } AS this0 + RETURN this0 AS this + UNION + MATCH (this1:Series) + WHERE ((this1.title = $param1 AND this1:Series) OR this1:Movie) + WITH this1 { .title, __resolveType: \\"Series\\", __id: id(this1) } AS this1 + RETURN this1 AS this + UNION + MATCH (this2:Cartoon) + WHERE ((this2.title = $param2 AND this2:Series) OR this2:Movie) + WITH this2 { .title, __resolveType: \\"Cartoon\\", __id: id(this2) } AS this2 + RETURN this2 AS this + } + WITH this + RETURN this AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\", + \\"param1\\": \\"The Matrix\\", + \\"param2\\": \\"The Matrix\\" + }" + `); + }); + + test("top-level + connection where", async () => { + const query = gql` + { + actors( + where: { + actedInConnection_SOME: { + OR: [ + { edge: { screenTime: 2 } } + { node: { OR: [{ title: "The Matrix" }, { typename_IN: [Series] }] } } + ] + } + } + ) { + actedIn { + title + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + WHERE EXISTS { + MATCH (this)-[this0:ACTED_IN]->(this1) + WHERE ((this0.screenTime = $param0 OR (this1.title = $param1 OR this1:Series)) AND (this1:Movie OR this1:Series OR this1:Cartoon)) + } + CALL { + WITH this + CALL { + WITH * + MATCH (this)-[this2:ACTED_IN]->(this3:Movie) + WITH this3 { .title, __resolveType: \\"Movie\\", __id: id(this3) } AS this3 + RETURN this3 AS var4 + UNION + WITH * + MATCH (this)-[this5:ACTED_IN]->(this6:Series) + WITH this6 { .title, __resolveType: \\"Series\\", __id: id(this6) } AS this6 + RETURN this6 AS var4 + UNION + WITH * + MATCH (this)-[this7:ACTED_IN]->(this8:Cartoon) + WITH this8 { .title, __resolveType: \\"Cartoon\\", __id: id(this8) } AS this8 + RETURN this8 AS var4 + } + WITH var4 + RETURN collect(var4) AS var4 + } + RETURN this { actedIn: var4 } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 2, + \\"high\\": 0 + }, + \\"param1\\": \\"The Matrix\\" + }" + `); + }); + + test("nested", async () => { + const query = gql` + { + actors { + actedIn( + where: { + OR: [ + { AND: [{ title: "The Matrix" }, { typename_IN: [Series] }] } + { typename_IN: [Movie] } + ] + } + ) { + title + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + CALL { + WITH this + CALL { + WITH * + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WHERE ((this1.title = $param0 AND this1:Series) OR this1:Movie) + WITH this1 { .title, __resolveType: \\"Movie\\", __id: id(this1) } AS this1 + RETURN this1 AS var2 + UNION + WITH * + MATCH (this)-[this3:ACTED_IN]->(this4:Series) + WHERE ((this4.title = $param1 AND this4:Series) OR this4:Movie) + WITH this4 { .title, __resolveType: \\"Series\\", __id: id(this4) } AS this4 + RETURN this4 AS var2 + UNION + WITH * + MATCH (this)-[this5:ACTED_IN]->(this6:Cartoon) + WHERE ((this6.title = $param2 AND this6:Series) OR this6:Movie) + WITH this6 { .title, __resolveType: \\"Cartoon\\", __id: id(this6) } AS this6 + RETURN this6 AS var2 + } + WITH var2 + RETURN collect(var2) AS var2 + } + RETURN this { actedIn: var2 } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\", + \\"param1\\": \\"The Matrix\\", + \\"param2\\": \\"The Matrix\\" + }" + `); + }); + + test("aggregation", async () => { + const query = gql` + { + productionsAggregate(where: { OR: [{ title: "the matrix" }, { typename_IN: [Movie, Series] }] }) { + count + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CALL { + MATCH (this0:Movie) + RETURN this0 AS node + UNION + MATCH (this1:Series) + RETURN this1 AS node + UNION + MATCH (this2:Cartoon) + RETURN this2 AS node + } + WITH * + WHERE (node.title = $param0 OR (node:Movie OR node:Series)) + RETURN count(node) AS this3 + } + RETURN { count: this3 }" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"the matrix\\" + }" + `); + }); + + test("nested aggregation", async () => { + const query = gql` + { + actors { + actedInAggregate(where: { typename_IN: [Movie, Series] }) { + count + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + CALL { + WITH this + CALL { + WITH this + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + RETURN this1 AS node, this0 AS edge + UNION + WITH this + MATCH (this)-[this2:ACTED_IN]->(this3:Series) + RETURN this3 AS node, this2 AS edge + UNION + WITH this + MATCH (this)-[this4:ACTED_IN]->(this5:Cartoon) + RETURN this5 AS node, this4 AS edge + } + WITH * + WHERE (node:Movie OR node:Series) + RETURN count(node) AS this6 + } + RETURN this { actedInAggregate: { count: this6 } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); + }); +}); diff --git a/packages/graphql/tests/tck/experimental/union-authorization.test.ts b/packages/graphql/tests/tck/experimental/union-authorization.test.ts index f5d5f5afdec..85c104ab6ba 100644 --- a/packages/graphql/tests/tck/experimental/union-authorization.test.ts +++ b/packages/graphql/tests/tck/experimental/union-authorization.test.ts @@ -17,11 +17,11 @@ * limitations under the License. */ -import { gql } from "graphql-tag"; import type { DocumentNode } from "graphql"; +import { gql } from "graphql-tag"; import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; import { createBearerToken } from "../../utils/create-bearer-token"; +import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; describe("Union top level operations with authorization", () => { const secret = "secret"; @@ -81,7 +81,8 @@ describe("Union top level operations with authorization", () => { WITH this1 { .title, __resolveType: \\"Movie\\", __id: id(this1) } AS this1 RETURN this1 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -145,7 +146,8 @@ describe("Union top level operations with authorization", () => { WITH this1 { .title, search: var4, __resolveType: \\"Movie\\", __id: id(this1) } AS this1 RETURN this1 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -210,7 +212,8 @@ describe("Union top level operations with authorization", () => { WITH this1 { .title, search: var4, __resolveType: \\"Movie\\", __id: id(this1) } AS this1 RETURN this1 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -271,7 +274,8 @@ describe("Union top level operations with authorization", () => { WITH this0 { .title, search: var3, __resolveType: \\"Movie\\", __id: id(this0) } AS this0 RETURN this0 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -335,7 +339,8 @@ describe("Union top level operations with authorization", () => { WITH this0 { .title, search: var5, __resolveType: \\"Movie\\", __id: id(this0) } AS this0 RETURN this0 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/experimental/union-relationship-filtering.test.ts b/packages/graphql/tests/tck/experimental/union-relationship-filtering.test.ts new file mode 100644 index 00000000000..d010af3ac26 --- /dev/null +++ b/packages/graphql/tests/tck/experimental/union-relationship-filtering.test.ts @@ -0,0 +1,141 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { DocumentNode } from "graphql"; +import { gql } from "graphql-tag"; +import { Neo4jGraphQL } from "../../../src"; +import { createBearerToken } from "../../utils/create-bearer-token"; +import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; + +describe("Union relationship filtering operations", () => { + const secret = "secret"; + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeEach(() => { + typeDefs = gql` + union Production = Movie | Series + + type Movie { + title: String! + cost: Float + runtime: Int + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Series { + title: String! + episodes: Int + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Actor { + name: String! + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + + interface ActedIn @relationshipProperties { + screenTime: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + features: { authorization: { key: secret } }, + experimental: true, + }); + }); + + test("Union filter (top level)", async () => { + const query = gql` + query actedInWhere { + productions(where: { Movie: { title: "The Office" }, Series: { title: "The Office 2" } }) { + ... on Movie { + title + } + ... on Series { + title + } + } + } + `; + + const token = createBearerToken(secret); + const result = await translateQuery(neoSchema, query, { token }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + MATCH (this0:Movie) + WHERE this0.title = $param0 + WITH this0 { .title, __resolveType: \\"Movie\\", __id: id(this0) } AS this0 + RETURN this0 AS this + UNION + MATCH (this1:Series) + WHERE this1.title = $param1 + WITH this1 { .title, __resolveType: \\"Series\\", __id: id(this1) } AS this1 + RETURN this1 AS this + } + WITH this + RETURN this AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Office\\", + \\"param1\\": \\"The Office 2\\" + }" + `); + }); + + test("Filtering on nested-level relationship unions", async () => { + const query = gql` + query actedInWhere { + actors( + where: { + actedIn_SOME: { Movie: { title_CONTAINS: "The Office" }, Series: { title_ENDS_WITH: "Office" } } + } + ) { + name + } + } + `; + + const token = createBearerToken(secret); + const result = await translateQuery(neoSchema, query, { token }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + WHERE (EXISTS { + MATCH (this)-[:ACTED_IN]->(this0:Movie) + WHERE this0.title CONTAINS $param0 + } AND EXISTS { + MATCH (this)-[:ACTED_IN]->(this1:Series) + WHERE this1.title ENDS WITH $param1 + }) + RETURN this { .name } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Office\\", + \\"param1\\": \\"Office\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/experimental/union-top-level.test.ts b/packages/graphql/tests/tck/experimental/union-top-level.test.ts index 2bd74aa1393..0caaf32aff5 100644 --- a/packages/graphql/tests/tck/experimental/union-top-level.test.ts +++ b/packages/graphql/tests/tck/experimental/union-top-level.test.ts @@ -66,17 +66,18 @@ describe("Union top level operations", () => { const result = await translateQuery(neoSchema, query, { token }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL { - MATCH (this0:Genre) - WITH this0 { .name, __resolveType: \\"Genre\\", __id: id(this0) } AS this0 - RETURN this0 AS this - UNION - MATCH (this1:Movie) - WITH this1 { .title, __resolveType: \\"Movie\\", __id: id(this1) } AS this1 - RETURN this1 AS this - } - RETURN this" - `); + "CALL { + MATCH (this0:Genre) + WITH this0 { .name, __resolveType: \\"Genre\\", __id: id(this0) } AS this0 + RETURN this0 AS this + UNION + MATCH (this1:Movie) + WITH this1 { .title, __resolveType: \\"Movie\\", __id: id(this1) } AS this1 + RETURN this1 AS this + } + WITH this + RETURN this AS this" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); }); @@ -129,7 +130,8 @@ describe("Union top level operations", () => { WITH this1 { .title, search: var4, __resolveType: \\"Movie\\", __id: id(this1) } AS this1 RETURN this1 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -184,7 +186,8 @@ describe("Union top level operations", () => { WITH this1 { .title, search: var4, __resolveType: \\"Movie\\", __id: id(this1) } AS this1 RETURN this1 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -239,7 +242,8 @@ describe("Union top level operations", () => { WITH this0 { .title, search: var3, __resolveType: \\"Movie\\", __id: id(this0) } AS this0 RETURN this0 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -297,7 +301,8 @@ describe("Union top level operations", () => { WITH this0 { .title, search: var5, __resolveType: \\"Movie\\", __id: id(this0) } AS this0 RETURN this0 AS this } - RETURN this" + WITH this + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -363,9 +368,10 @@ describe("Union top level operations", () => { WITH this1 { .title, search: var4, __resolveType: \\"Movie\\", __id: id(this1) } AS this1 RETURN this1 AS this } - RETURN this + WITH this SKIP $param3 - LIMIT $param4" + LIMIT $param4 + RETURN this AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/issues/1150.test.ts b/packages/graphql/tests/tck/issues/1150.test.ts index eb9091a449d..15b02b5c6e4 100644 --- a/packages/graphql/tests/tck/issues/1150.test.ts +++ b/packages/graphql/tests/tck/issues/1150.test.ts @@ -113,31 +113,36 @@ describe("https://github.com/neo4j/graphql/issues/1150", () => { WITH this MATCH (this)-[this0:CONSISTS_OF]->(this1:DriveComposition) WHERE this0.current = $param1 + WITH collect({ node: this1, relationship: this0 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this1 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 CALL { WITH this1 - MATCH (this1)-[this2:HAS]->(this3:Battery) - WHERE (this2.current = $param2 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - WITH { current: this2.current, node: { __resolveType: \\"Battery\\", __id: id(this3), id: this3.id } } AS edge - RETURN edge - UNION - WITH this1 - MATCH (this1)-[this4:HAS]->(this5:CombustionEngine) - WHERE this4.current = $param6 - WITH { current: this4.current, node: { __resolveType: \\"CombustionEngine\\", __id: id(this5), id: this5.id } } AS edge - RETURN edge + CALL { + WITH this1 + MATCH (this1)-[this2:HAS]->(this3:Battery) + WHERE (this2.current = $param2 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param5 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WITH { current: this2.current, node: { __resolveType: \\"Battery\\", __id: id(this3), id: this3.id } } AS edge + RETURN edge + UNION + WITH this1 + MATCH (this1)-[this4:HAS]->(this5:CombustionEngine) + WHERE this4.current = $param6 + WITH { current: this4.current, node: { __resolveType: \\"CombustionEngine\\", __id: id(this5), id: this5.id } } AS edge + RETURN edge + } + WITH collect(edge) AS edges + WITH edges, size(edges) AS totalCount + RETURN { edges: edges, totalCount: totalCount } AS var6 } - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var6 + RETURN collect({ node: { driveComponentConnection: var6 } }) AS var7 } - WITH { node: { driveComponentConnection: var6 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var7 + RETURN { edges: var7, totalCount: totalCount } AS var8 } - RETURN this { .current, driveCompositionsConnection: var7 } AS this" + RETURN this { .current, driveCompositionsConnection: var8 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/issues/1221.test.ts b/packages/graphql/tests/tck/issues/1221.test.ts index e628d6d2ea9..830337307a3 100644 --- a/packages/graphql/tests/tck/issues/1221.test.ts +++ b/packages/graphql/tests/tck/issues/1221.test.ts @@ -91,21 +91,31 @@ describe("https://github.com/neo4j/graphql/issues/1221", () => { WITH this MATCH (this)-[this4:ARCHITECTURE]->(this5:MasterData) WHERE this4.current = $param2 + WITH collect({ node: this5, relationship: this4 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this5 - MATCH (this5)-[this6:HAS_NAME]->(this7:NameDetails) - WHERE this6.current = $param3 - WITH { node: { fullName: this7.fullName } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var8 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this5, edge.relationship AS this4 + CALL { + WITH this5 + MATCH (this5)-[this6:HAS_NAME]->(this7:NameDetails) + WHERE this6.current = $param3 + WITH collect({ node: this7, relationship: this6 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this7, edge.relationship AS this6 + RETURN collect({ node: { fullName: this7.fullName } }) AS var8 + } + RETURN { edges: var8, totalCount: totalCount } AS var9 + } + RETURN collect({ node: { nameDetailsConnection: var9 } }) AS var10 } - WITH { node: { nameDetailsConnection: var8 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var9 + RETURN { edges: var10, totalCount: totalCount } AS var11 } - RETURN this { .id, architectureConnection: var9 } AS this" + RETURN this { .id, architectureConnection: var11 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -202,30 +212,45 @@ describe("https://github.com/neo4j/graphql/issues/1221", () => { WITH this MATCH (this)-[this6:MAIN]->(this7:Series) WHERE this6.current = $param2 + WITH collect({ node: this7, relationship: this6 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this7 - MATCH (this7)-[this8:ARCHITECTURE]->(this9:MasterData) - WHERE this8.current = $param3 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this7, edge.relationship AS this6 CALL { - WITH this9 - MATCH (this9)-[this10:HAS_NAME]->(this11:NameDetails) - WHERE this10.current = $param4 - WITH { node: { fullName: this11.fullName } } AS edge - WITH collect(edge) AS edges + WITH this7 + MATCH (this7)-[this8:ARCHITECTURE]->(this9:MasterData) + WHERE this8.current = $param3 + WITH collect({ node: this9, relationship: this8 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var12 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this9, edge.relationship AS this8 + CALL { + WITH this9 + MATCH (this9)-[this10:HAS_NAME]->(this11:NameDetails) + WHERE this10.current = $param4 + WITH collect({ node: this11, relationship: this10 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this11, edge.relationship AS this10 + RETURN collect({ node: { fullName: this11.fullName } }) AS var12 + } + RETURN { edges: var12, totalCount: totalCount } AS var13 + } + RETURN collect({ node: { nameDetailsConnection: var13 } }) AS var14 + } + RETURN { edges: var14, totalCount: totalCount } AS var15 } - WITH { node: { nameDetailsConnection: var12 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var13 + RETURN collect({ node: { architectureConnection: var15 } }) AS var16 } - WITH { node: { architectureConnection: var13 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var14 + RETURN { edges: var16, totalCount: totalCount } AS var17 } - RETURN this { .id, mainConnection: var14 } AS this" + RETURN this { .id, mainConnection: var17 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/issues/1249.test.ts b/packages/graphql/tests/tck/issues/1249.test.ts index 56301a4bece..6303612cb94 100644 --- a/packages/graphql/tests/tck/issues/1249.test.ts +++ b/packages/graphql/tests/tck/issues/1249.test.ts @@ -88,15 +88,20 @@ describe("https://github.com/neo4j/graphql/issues/1249", () => { CALL { WITH this1 MATCH (this1)-[this2:MATERIAL_SUPPLIER]->(this3:Supplier) - WITH { supplierMaterialNumber: this2.supplierMaterialNumber, node: { supplierId: this3.supplierId } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this3, relationship: this2 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var4 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this3, edge.relationship AS this2 + RETURN collect({ supplierMaterialNumber: this2.supplierMaterialNumber, node: { supplierId: this3.supplierId } }) AS var4 + } + RETURN { edges: var4, totalCount: totalCount } AS var5 } - WITH this1 { .id, suppliersConnection: var4 } AS this1 - RETURN head(collect(this1)) AS var5 + WITH this1 { .id, suppliersConnection: var5 } AS this1 + RETURN head(collect(this1)) AS var6 } - RETURN this { .supplierMaterialNumber, material: var5 } AS this" + RETURN this { .supplierMaterialNumber, material: var6 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/issues/1364.test.ts b/packages/graphql/tests/tck/issues/1364.test.ts index 125a4c5deee..3b5ae6c01f4 100644 --- a/packages/graphql/tests/tck/issues/1364.test.ts +++ b/packages/graphql/tests/tck/issues/1364.test.ts @@ -93,27 +93,29 @@ describe("https://github.com/neo4j/graphql/issues/1364", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH collect(this) AS edges + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount - WITH * - ORDER BY this.title ASC CALL { - WITH this + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.title ASC CALL { - WITH this - WITH this AS this - MATCH (this)-[:HAS_GENRE]->(genre:Genre) - RETURN count(DISTINCT genre) as result + WITH this0 + CALL { + WITH this0 + WITH this0 AS this + MATCH (this)-[:HAS_GENRE]->(genre:Genre) + RETURN count(DISTINCT genre) as result + } + UNWIND result AS this1 + RETURN head(collect(this1)) AS this1 } - UNWIND result AS this0 - RETURN head(collect(this0)) AS this0 + RETURN collect({ node: { title: this0.title, totalGenres: this1 } }) AS var2 } - WITH { node: this { .title, totalGenres: this0 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var2, totalCount: totalCount } AS this" `); }); @@ -134,27 +136,29 @@ describe("https://github.com/neo4j/graphql/issues/1364", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH collect(this) AS edges + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 CALL { - WITH this - WITH this AS this - MATCH (this)-[:HAS_GENRE]->(genre:Genre) - RETURN count(DISTINCT genre) as result + WITH this0 + CALL { + WITH this0 + WITH this0 AS this + MATCH (this)-[:HAS_GENRE]->(genre:Genre) + RETURN count(DISTINCT genre) as result + } + UNWIND result AS this1 + RETURN head(collect(this1)) AS this1 } - UNWIND result AS this0 - RETURN head(collect(this0)) AS this0 + WITH * + ORDER BY this1 ASC + RETURN collect({ node: { title: this0.title, totalGenres: this1 } }) AS var2 } - WITH * - ORDER BY this0 ASC - WITH { node: this { .title, totalGenres: this0 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var2, totalCount: totalCount } AS this" `); }); @@ -176,38 +180,40 @@ describe("https://github.com/neo4j/graphql/issues/1364", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH collect(this) AS edges + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 CALL { - WITH this - WITH this AS this - MATCH (this)-[:HAS_GENRE]->(genre:Genre) - RETURN count(DISTINCT genre) as result + WITH this0 + CALL { + WITH this0 + WITH this0 AS this + MATCH (this)-[:HAS_GENRE]->(genre:Genre) + RETURN count(DISTINCT genre) as result + } + UNWIND result AS this1 + RETURN head(collect(this1)) AS this1 } - UNWIND result AS this0 - RETURN head(collect(this0)) AS this0 - } - WITH * - ORDER BY this0 ASC - CALL { - WITH this + WITH * + ORDER BY this1 ASC CALL { - WITH this - WITH this AS this - MATCH (this)<-[:ACTED_IN]-(actor:Actor) - RETURN count(DISTINCT actor) as result + WITH this0 + CALL { + WITH this0 + WITH this0 AS this + MATCH (this)<-[:ACTED_IN]-(actor:Actor) + RETURN count(DISTINCT actor) as result + } + UNWIND result AS this2 + RETURN head(collect(this2)) AS this2 } - UNWIND result AS this1 - RETURN head(collect(this1)) AS this1 + RETURN collect({ node: { title: this0.title, totalGenres: this1, totalActors: this2 } }) AS var3 } - WITH { node: this { .title, totalGenres: this0, totalActors: this1 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var3, totalCount: totalCount } AS this" `); }); }); diff --git a/packages/graphql/tests/tck/issues/1528.test.ts b/packages/graphql/tests/tck/issues/1528.test.ts index 74019eef180..5fc763201bc 100644 --- a/packages/graphql/tests/tck/issues/1528.test.ts +++ b/packages/graphql/tests/tck/issues/1528.test.ts @@ -74,31 +74,28 @@ describe("https://github.com/neo4j/graphql/issues/1528", () => { CALL { WITH this MATCH (this)<-[this0:IS_GENRE]-(this1:Movie) - WITH this0, this1 - ORDER BY this1.actorsCount DESC - CALL { - WITH this1 - CALL { - WITH this1 - WITH this1 AS this - MATCH (this)<-[:ACTED_IN]-(ac:Person) - RETURN count(ac) as res - } - UNWIND res AS this2 - RETURN head(collect(this2)) AS this2 - } - WITH { node: { title: this1.title, actorsCount: this2 } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge - ORDER BY edge.node.actorsCount DESC - RETURN collect(edge) AS var3 + WITH edge.node AS this1, edge.relationship AS this0 + CALL { + WITH this1 + CALL { + WITH this1 + WITH this1 AS this + MATCH (this)<-[:ACTED_IN]-(ac:Person) + RETURN count(ac) as res + } + UNWIND res AS this2 + RETURN head(collect(this2)) AS this2 + } + WITH * + ORDER BY this2 DESC + RETURN collect({ node: { title: this1.title, actorsCount: this2 } }) AS var3 } - WITH var3 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var4 + RETURN { edges: var3, totalCount: totalCount } AS var4 } RETURN this { moviesConnection: var4 } AS this" `); diff --git a/packages/graphql/tests/tck/issues/1760.test.ts b/packages/graphql/tests/tck/issues/1760.test.ts index 57c9e7db6fd..b90b290b0b8 100644 --- a/packages/graphql/tests/tck/issues/1760.test.ts +++ b/packages/graphql/tests/tck/issues/1760.test.ts @@ -153,39 +153,59 @@ describe("https://github.com/neo4j/graphql/issues/1760", () => { WITH this MATCH (this)-[this1:HAS_NAME]->(this2:NameDetails) WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param6 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { fullName: this2.fullName } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this2, relationship: this1 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { fullName: this2.fullName } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 } CALL { WITH this - MATCH (this)-[this4:HAS_MARKETS]->(this5:Market) + MATCH (this)-[this5:HAS_MARKETS]->(this6:Market) WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param7 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH collect({ node: this6, relationship: this5 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this5 - MATCH (this5)-[this6:HAS_NAME]->(this7:NameDetails) - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { fullName: this7.fullName } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var8 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this6, edge.relationship AS this5 + CALL { + WITH this6 + MATCH (this6)-[this7:HAS_NAME]->(this8:NameDetails) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param8 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH collect({ node: this8, relationship: this7 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this8, edge.relationship AS this7 + RETURN collect({ node: { fullName: this8.fullName } }) AS var9 + } + RETURN { edges: var9, totalCount: totalCount } AS var10 + } + RETURN collect({ node: { nameDetailsConnection: var10 } }) AS var11 } - WITH { node: { nameDetailsConnection: var8 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var9 + RETURN { edges: var11, totalCount: totalCount } AS var12 } CALL { WITH this - MATCH (this)<-[this10:HAS_BASE]-(this11:BaseObject) + MATCH (this)<-[this13:HAS_BASE]-(this14:BaseObject) WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param9 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { node: { id: this11.id } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this14, relationship: this13 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var12 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this14, edge.relationship AS this13 + RETURN collect({ node: { id: this14.id } }) AS var15 + } + RETURN { edges: var15, totalCount: totalCount } AS var16 } - RETURN this { relatedId: this0, nameDetailsConnection: var3, marketsConnection: var9, baseObjectConnection: var12 } AS this" + RETURN this { relatedId: this0, nameDetailsConnection: var4, marketsConnection: var12, baseObjectConnection: var16 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/issues/1783.test.ts b/packages/graphql/tests/tck/issues/1783.test.ts index 41c1174e599..adf9276b2d1 100644 --- a/packages/graphql/tests/tck/issues/1783.test.ts +++ b/packages/graphql/tests/tck/issues/1783.test.ts @@ -126,30 +126,45 @@ describe("https://github.com/neo4j/graphql/issues/1783", () => { WITH this MATCH (this)-[this6:HAS_NAME]->(this7:NameDetails) WHERE this6.current = $param6 - WITH { node: { fullName: this7.fullName } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this7, relationship: this6 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var8 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this7, edge.relationship AS this6 + RETURN collect({ node: { fullName: this7.fullName } }) AS var8 + } + RETURN { edges: var8, totalCount: totalCount } AS var9 } CALL { WITH this - MATCH (this)-[this9:ARCHITECTURE]->(this10:MasterData) - WHERE this9.current = $param7 + MATCH (this)-[this10:ARCHITECTURE]->(this11:MasterData) + WHERE this10.current = $param7 + WITH collect({ node: this11, relationship: this10 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this10 - MATCH (this10)-[this11:HAS_NAME]->(this12:NameDetails) - WHERE this11.current = $param8 - WITH { node: { fullName: this12.fullName } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var13 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this11, edge.relationship AS this10 + CALL { + WITH this11 + MATCH (this11)-[this12:HAS_NAME]->(this13:NameDetails) + WHERE this12.current = $param8 + WITH collect({ node: this13, relationship: this12 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this13, edge.relationship AS this12 + RETURN collect({ node: { fullName: this13.fullName } }) AS var14 + } + RETURN { edges: var14, totalCount: totalCount } AS var15 + } + RETURN collect({ node: { nameDetailsConnection: var15 } }) AS var16 } - WITH { node: { nameDetailsConnection: var13 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var14 + RETURN { edges: var16, totalCount: totalCount } AS var17 } - RETURN this { .id, nameDetailsConnection: var8, architectureConnection: var14 } AS this" + RETURN this { .id, nameDetailsConnection: var9, architectureConnection: var17 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/issues/2022.test.ts b/packages/graphql/tests/tck/issues/2022.test.ts index e31d11339a1..3ab59de2ad3 100644 --- a/packages/graphql/tests/tck/issues/2022.test.ts +++ b/packages/graphql/tests/tck/issues/2022.test.ts @@ -90,35 +90,36 @@ describe("https://github.com/neo4j/graphql/issues/2022", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:ArtPiece) - WITH collect(this) AS edges + "MATCH (this0:ArtPiece) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount CALL { - WITH this - MATCH (this)-[this0:SOLD_AT_AUCTION_AS]->(this1:AuctionItem) + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 CALL { - WITH this1 - MATCH (this1)<-[this2:BOUGHT_ITEM_AT_AUCTION]-(this3:Organization) - WITH this3 { .name, dbId: this3.id } AS this3 - RETURN head(collect(this3)) AS var4 + WITH this0 + MATCH (this0)-[this1:SOLD_AT_AUCTION_AS]->(this2:AuctionItem) + CALL { + WITH this2 + MATCH (this2)<-[this3:BOUGHT_ITEM_AT_AUCTION]-(this4:Organization) + WITH this4 { .name, dbId: this4.id } AS this4 + RETURN head(collect(this4)) AS var5 + } + WITH this2 { .auctionName, .lotNumber, dbId: this2.id, buyer: var5 } AS this2 + RETURN head(collect(this2)) AS var6 } - WITH this1 { .auctionName, .lotNumber, dbId: this1.id, buyer: var4 } AS this1 - RETURN head(collect(this1)) AS var5 - } - CALL { - WITH this - MATCH (this)-[this6:OWNED_BY]->(this7:Organization) - WITH this7 { .name, dbId: this7.id } AS this7 - RETURN head(collect(this7)) AS var8 + CALL { + WITH this0 + MATCH (this0)-[this7:OWNED_BY]->(this8:Organization) + WITH this8 { .name, dbId: this8.id } AS this8 + RETURN head(collect(this8)) AS var9 + } + RETURN collect({ node: { dbId: this0.id, title: this0.title, auction: var6, owner: var9 } }) AS var10 } - WITH { node: this { .title, dbId: this.id, auction: var5, owner: var8 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var10, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); }); - }); diff --git a/packages/graphql/tests/tck/issues/2262.test.ts b/packages/graphql/tests/tck/issues/2262.test.ts index 96b972ae9aa..6ad8b5a6bec 100644 --- a/packages/graphql/tests/tck/issues/2262.test.ts +++ b/packages/graphql/tests/tck/issues/2262.test.ts @@ -76,30 +76,32 @@ describe("https://github.com/neo4j/graphql/issues/2262", () => { CALL { WITH this MATCH (this)<-[this0:OUTPUT]-(this1:Process) + WITH collect({ node: this1, relationship: this0 }) AS edges + WITH edges, size(edges) AS totalCount CALL { - WITH this1 - MATCH (this1)<-[this2:INPUT]-(this3:Component) - WITH this2, this3 - ORDER BY this3.uuid DESC - WITH { node: { uuid: this3.uuid } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 CALL { - WITH edges - UNWIND edges AS edge - WITH edge - ORDER BY edge.node.uuid DESC - RETURN collect(edge) AS var4 + WITH this1 + MATCH (this1)<-[this2:INPUT]-(this3:Component) + WITH collect({ node: this3, relationship: this2 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this3, edge.relationship AS this2 + WITH * + ORDER BY this3.uuid DESC + RETURN collect({ node: { uuid: this3.uuid } }) AS var4 + } + RETURN { edges: var4, totalCount: totalCount } AS var5 } - WITH var4 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var5 + RETURN collect({ node: { uuid: this1.uuid, componentInputsConnection: var5 } }) AS var6 } - WITH { node: { uuid: this1.uuid, componentInputsConnection: var5 } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var6 + RETURN { edges: var6, totalCount: totalCount } AS var7 } - RETURN this { .uuid, upstreamProcessConnection: var6 } AS this" + RETURN this { .uuid, upstreamProcessConnection: var7 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/issues/2437.test.ts b/packages/graphql/tests/tck/issues/2437.test.ts index b4537c25fef..6f122af6ea1 100644 --- a/packages/graphql/tests/tck/issues/2437.test.ts +++ b/packages/graphql/tests/tck/issues/2437.test.ts @@ -86,18 +86,17 @@ describe("https://github.com/neo4j/graphql/issues/2437", () => { WITH this MATCH (this)-[this0:IS_VALUATION_AGENT]->(this1:Valuation) WHERE ($isAuthenticated = true AND this1.archivedAt IS NULL) - WITH { node: { uuid: this1.uuid } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge + WITH edge.node AS this1, edge.relationship AS this0 + WITH * LIMIT $param2 - RETURN collect(edge) AS var2 + RETURN collect({ node: { uuid: this1.uuid } }) AS var2 } - WITH var2 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN { edges: var2, totalCount: totalCount } AS var3 } RETURN this { .uuid, valuationsConnection: var3 } AS this" `); diff --git a/packages/graphql/tests/tck/issues/3394.test.ts b/packages/graphql/tests/tck/issues/3394.test.ts index af2aecb9733..e0580370e2c 100644 --- a/packages/graphql/tests/tck/issues/3394.test.ts +++ b/packages/graphql/tests/tck/issues/3394.test.ts @@ -113,16 +113,18 @@ describe("https://github.com/neo4j/graphql/issues/3394", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Product) - WITH collect(this) AS edges + "MATCH (this0:Product) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount - WITH * - ORDER BY this.fg_item DESC - WITH { node: this { .description, id: this.fg_item_id, partNumber: this.fg_item } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.fg_item DESC + RETURN collect({ node: { id: this0.fg_item_id, partNumber: this0.fg_item, description: this0.description } }) AS var1 + } + RETURN { edges: var1, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -152,20 +154,17 @@ describe("https://github.com/neo4j/graphql/issues/3394", () => { CALL { WITH this MATCH (this)-[this0:CAN_ACCESS]->(this1:Product) - WITH this0, this1 - ORDER BY this1.partNumber DESC - WITH { node: { id: this1.fg_item_id, partNumber: this1.fg_item, description: this1.description } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge - ORDER BY edge.node.partNumber DESC - RETURN collect(edge) AS var2 + WITH edge.node AS this1, edge.relationship AS this0 + WITH * + ORDER BY this1.fg_item DESC + RETURN collect({ node: { id: this1.fg_item_id, partNumber: this1.fg_item, description: this1.description } }) AS var2 } - WITH var2 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN { edges: var2, totalCount: totalCount } AS var3 } RETURN this { productsConnection: var3 } AS this" `); diff --git a/packages/graphql/tests/tck/issues/3901.test.ts b/packages/graphql/tests/tck/issues/3901.test.ts index 459d5faed65..19d147c6717 100644 --- a/packages/graphql/tests/tck/issues/3901.test.ts +++ b/packages/graphql/tests/tck/issues/3901.test.ts @@ -167,17 +167,17 @@ describe("https://github.com/neo4j/graphql/issues/3901", () => { WITH * CALL { WITH this0_seasons0_node - MATCH (this0_seasons0_node)-[:SEASON_OF]->(authorization_1_1_0_0_after_this1:Serie) - OPTIONAL MATCH (authorization_1_1_0_0_after_this1)<-[:PUBLISHER]-(authorization_1_1_0_0_after_this2:User) - WITH *, count(authorization_1_1_0_0_after_this2) AS publisherCount + MATCH (this0_seasons0_node)-[:SEASON_OF]->(authorization_0_1_0_0_after_this1:Serie) + OPTIONAL MATCH (authorization_0_1_0_0_after_this1)<-[:PUBLISHER]-(authorization_0_1_0_0_after_this2:User) + WITH *, count(authorization_0_1_0_0_after_this2) AS publisherCount WITH * - WHERE (publisherCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_1_1_0_0_after_this2.id = $jwt.sub)) - RETURN count(authorization_1_1_0_0_after_this1) = 1 AS authorization_1_1_0_0_after_var0 + WHERE (publisherCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_0_1_0_0_after_this2.id = $jwt.sub)) + RETURN count(authorization_0_1_0_0_after_this1) = 1 AS authorization_0_1_0_0_after_var0 } - OPTIONAL MATCH (this0)<-[:PUBLISHER]-(authorization_0_0_0_0_after_this0:User) - WITH *, count(authorization_0_0_0_0_after_this0) AS publisherCount + OPTIONAL MATCH (this0)<-[:PUBLISHER]-(authorization_0_after_this0:User) + WITH *, count(authorization_0_after_this0) AS publisherCount WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (authorization_1_1_0_0_after_var0 = true AND ($jwt.roles IS NOT NULL AND $authorization_1_1_0_0_after_param2 IN $jwt.roles) AND ($jwt.roles IS NOT NULL AND $authorization_1_1_0_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ((publisherCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_0_0_0_0_after_this0.id = $jwt.sub)) AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles) AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (authorization_0_1_0_0_after_var0 = true AND ($jwt.roles IS NOT NULL AND $authorization_0_1_0_0_after_param2 IN $jwt.roles) AND ($jwt.roles IS NOT NULL AND $authorization_0_1_0_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ((publisherCount <> 0 AND ($jwt.sub IS NOT NULL AND authorization_0_after_this0.id = $jwt.sub)) AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles) AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param3 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this0 } CALL { @@ -199,11 +199,11 @@ describe("https://github.com/neo4j/graphql/issues/3901", () => { \\"roles\\": [], \\"sub\\": \\"michel\\" }, - \\"authorization_1_1_0_0_after_param2\\": \\"verified\\", - \\"authorization_1_1_0_0_after_param3\\": \\"creator\\", + \\"authorization_0_1_0_0_after_param2\\": \\"verified\\", + \\"authorization_0_1_0_0_after_param3\\": \\"creator\\", \\"this0_publisher_connect0_node_param0\\": \\"ID\\", - \\"authorization_0_0_0_0_after_param2\\": \\"verified\\", - \\"authorization_0_0_0_0_after_param3\\": \\"creator\\", + \\"authorization_0_after_param2\\": \\"verified\\", + \\"authorization_0_after_param3\\": \\"creator\\", \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/issues/4007.test.ts b/packages/graphql/tests/tck/issues/4007.test.ts index d4ed15fb218..c22e9275314 100644 --- a/packages/graphql/tests/tck/issues/4007.test.ts +++ b/packages/graphql/tests/tck/issues/4007.test.ts @@ -66,12 +66,17 @@ describe("https://github.com/neo4j/graphql/issues/4007", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { node: { na: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { na: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { t: var2 } AS this" + RETURN this { t: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); diff --git a/packages/graphql/tests/tck/issues/4015.test.ts b/packages/graphql/tests/tck/issues/4015.test.ts index ff7dc95721a..330302638ca 100644 --- a/packages/graphql/tests/tck/issues/4015.test.ts +++ b/packages/graphql/tests/tck/issues/4015.test.ts @@ -70,12 +70,17 @@ describe("https://github.com/neo4j/graphql/issues/4015", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { node: { surname: this1.surname, name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { surname: this1.surname, name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { actorsConnection: var2 } AS this" + RETURN this { actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); diff --git a/packages/graphql/tests/tck/issues/4118.test.ts b/packages/graphql/tests/tck/issues/4118.test.ts index db800738d35..ddf5947ceb3 100644 --- a/packages/graphql/tests/tck/issues/4118.test.ts +++ b/packages/graphql/tests/tck/issues/4118.test.ts @@ -131,7 +131,7 @@ describe("https://github.com/neo4j/graphql/issues/2871", () => { CALL { WITH this0 OPTIONAL MATCH (this0_host_connect0_node:Tenant) - WHERE this0_host_connect0_node.id = $this0_host_connect0_node_param0 AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND size([(this0_host_connect0_node)<-[:ADMIN_IN]-(authorization_0_0_0_0_before_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_before_this0.userId = $jwt.id) | 1]) > 0) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_before_param2 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE this0_host_connect0_node.id = $this0_host_connect0_node_param0 AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND size([(this0_host_connect0_node)<-[:ADMIN_IN]-(authorization_0_before_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_before_this0.userId = $jwt.id) | 1]) > 0) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_before_param2 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) CALL { WITH * WITH collect(this0_host_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -144,10 +144,10 @@ describe("https://github.com/neo4j/graphql/issues/2871", () => { } WITH this0, this0_host_connect0_node WITH * - OPTIONAL MATCH (this0)-[:HOSTED_BY]->(authorization_0_0_0_0_after_this1:Tenant) - WITH *, count(authorization_0_0_0_0_after_this1) AS hostCount + OPTIONAL MATCH (this0)-[:HOSTED_BY]->(authorization_0_after_this1:Tenant) + WITH *, count(authorization_0_after_this1) AS hostCount WITH * - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (hostCount <> 0 AND size([(authorization_0_0_0_0_after_this1)<-[:ADMIN_IN]-(authorization_0_0_0_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND size([(this0_host_connect0_node)<-[:ADMIN_IN]-(authorization_0_0_0_0_after_this2:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_after_this2.userId = $jwt.id) | 1]) > 0) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (hostCount <> 0 AND size([(authorization_0_after_this1)<-[:ADMIN_IN]-(authorization_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT (($isAuthenticated = true AND size([(this0_host_connect0_node)<-[:ADMIN_IN]-(authorization_0_after_this2:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_after_this2.userId = $jwt.id) | 1]) > 0) OR ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) RETURN count(*) AS connect_this0_host_connect_Tenant0 } WITH * @@ -156,15 +156,15 @@ describe("https://github.com/neo4j/graphql/issues/2871", () => { OPTIONAL MATCH (this0_openingDays_connect0_node:OpeningDay) CALL { WITH this0_openingDays_connect0_node - MATCH (this0_openingDays_connect0_node)<-[:VALID_OPENING_DAYS]-(authorization_0_0_0_0_before_this1:Settings) - OPTIONAL MATCH (authorization_0_0_0_0_before_this1)<-[:HAS_SETTINGS]-(authorization_0_0_0_0_before_this2:Tenant) - WITH *, count(authorization_0_0_0_0_before_this2) AS tenantCount + MATCH (this0_openingDays_connect0_node)<-[:VALID_OPENING_DAYS]-(authorization_0_before_this1:Settings) + OPTIONAL MATCH (authorization_0_before_this1)<-[:HAS_SETTINGS]-(authorization_0_before_this2:Tenant) + WITH *, count(authorization_0_before_this2) AS tenantCount WITH * - WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_before_this2)<-[:ADMIN_IN]-(authorization_0_0_0_0_before_this3:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_before_this3.userId = $jwt.id) | 1]) > 0) - RETURN count(authorization_0_0_0_0_before_this1) = 1 AS authorization_0_0_0_0_before_var0 + WHERE (tenantCount <> 0 AND size([(authorization_0_before_this2)<-[:ADMIN_IN]-(authorization_0_before_this3:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_before_this3.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_before_this1) = 1 AS authorization_0_before_var0 } WITH * - WHERE this0_openingDays_connect0_node.id = $this0_openingDays_connect0_node_param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_before_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE this0_openingDays_connect0_node.id = $this0_openingDays_connect0_node_param0 AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_before_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) CALL { WITH * WITH collect(this0_openingDays_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -177,19 +177,19 @@ describe("https://github.com/neo4j/graphql/issues/2871", () => { } WITH this0, this0_openingDays_connect0_node WITH * - OPTIONAL MATCH (this0)-[:HOSTED_BY]->(authorization_0_0_0_0_after_this1:Tenant) - WITH *, count(authorization_0_0_0_0_after_this1) AS hostCount + OPTIONAL MATCH (this0)-[:HOSTED_BY]->(authorization_0_after_this1:Tenant) + WITH *, count(authorization_0_after_this1) AS hostCount CALL { WITH this0_openingDays_connect0_node - MATCH (this0_openingDays_connect0_node)<-[:VALID_OPENING_DAYS]-(authorization_0_0_0_0_after_this3:Settings) - OPTIONAL MATCH (authorization_0_0_0_0_after_this3)<-[:HAS_SETTINGS]-(authorization_0_0_0_0_after_this4:Tenant) - WITH *, count(authorization_0_0_0_0_after_this4) AS tenantCount + MATCH (this0_openingDays_connect0_node)<-[:VALID_OPENING_DAYS]-(authorization_0_after_this3:Settings) + OPTIONAL MATCH (authorization_0_after_this3)<-[:HAS_SETTINGS]-(authorization_0_after_this4:Tenant) + WITH *, count(authorization_0_after_this4) AS tenantCount WITH * - WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_after_this4)<-[:ADMIN_IN]-(authorization_0_0_0_0_after_this5:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_after_this5.userId = $jwt.id) | 1]) > 0) - RETURN count(authorization_0_0_0_0_after_this3) = 1 AS authorization_0_0_0_0_after_var2 + WHERE (tenantCount <> 0 AND size([(authorization_0_after_this4)<-[:ADMIN_IN]-(authorization_0_after_this5:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_after_this5.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_after_this3) = 1 AS authorization_0_after_var2 } WITH * - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (hostCount <> 0 AND size([(authorization_0_0_0_0_after_this1)<-[:ADMIN_IN]-(authorization_0_0_0_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_after_var2 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (hostCount <> 0 AND size([(authorization_0_after_this1)<-[:ADMIN_IN]-(authorization_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_after_var2 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0])) RETURN count(*) AS connect_this0_openingDays_connect_OpeningDay0 } WITH * @@ -201,10 +201,10 @@ describe("https://github.com/neo4j/graphql/issues/2871", () => { RETURN c AS this0_host_Tenant_unique_ignored } WITH * - OPTIONAL MATCH (this0)-[:HOSTED_BY]->(authorization_0_0_0_0_after_this1:Tenant) - WITH *, count(authorization_0_0_0_0_after_this1) AS hostCount + OPTIONAL MATCH (this0)-[:HOSTED_BY]->(authorization_0_after_this1:Tenant) + WITH *, count(authorization_0_after_this1) AS hostCount WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (hostCount <> 0 AND size([(authorization_0_0_0_0_after_this1)<-[:ADMIN_IN]-(authorization_0_0_0_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (hostCount <> 0 AND size([(authorization_0_after_this1)<-[:ADMIN_IN]-(authorization_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this0 } CALL { @@ -227,8 +227,8 @@ describe("https://github.com/neo4j/graphql/issues/2871", () => { \\"jwt\\": {}, \\"create_param2\\": \\"overlord\\", \\"this0_host_connect0_node_param0\\": \\"userid\\", - \\"authorization_0_0_0_0_before_param2\\": \\"overlord\\", - \\"authorization_0_0_0_0_after_param2\\": \\"overlord\\", + \\"authorization_0_before_param2\\": \\"overlord\\", + \\"authorization_0_after_param2\\": \\"overlord\\", \\"this0_openingDays_connect0_node_param0\\": \\"openingdayid\\", \\"resolvedCallbacks\\": {} }" diff --git a/packages/graphql/tests/tck/issues/4170.test.ts b/packages/graphql/tests/tck/issues/4170.test.ts index 1351b88f20b..54630bd594c 100644 --- a/packages/graphql/tests/tck/issues/4170.test.ts +++ b/packages/graphql/tests/tck/issues/4170.test.ts @@ -182,33 +182,33 @@ describe("https://github.com/neo4j/graphql/issues/4170", () => { WITH * CALL { WITH this0_settings0_node_openingDays0_node_open0_node - MATCH (this0_settings0_node_openingDays0_node_open0_node)<-[:HAS_OPEN_INTERVALS]-(authorization_3_0_0_0_after_this1:OpeningDay) + MATCH (this0_settings0_node_openingDays0_node_open0_node)<-[:HAS_OPEN_INTERVALS]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this1:OpeningDay) CALL { - WITH authorization_3_0_0_0_after_this1 - MATCH (authorization_3_0_0_0_after_this1)<-[:VALID_GARAGES]-(authorization_3_0_0_0_after_this2:Settings) - OPTIONAL MATCH (authorization_3_0_0_0_after_this2)<-[:HAS_SETTINGS]-(authorization_3_0_0_0_after_this3:Tenant) - WITH *, count(authorization_3_0_0_0_after_this3) AS tenantCount + WITH authorization_0_0_0_0_0_0_0_0_0_0_after_this1 + MATCH (authorization_0_0_0_0_0_0_0_0_0_0_after_this1)<-[:VALID_GARAGES]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this2:Settings) + OPTIONAL MATCH (authorization_0_0_0_0_0_0_0_0_0_0_after_this2)<-[:HAS_SETTINGS]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this3:Tenant) + WITH *, count(authorization_0_0_0_0_0_0_0_0_0_0_after_this3) AS tenantCount WITH * - WHERE (tenantCount <> 0 AND size([(authorization_3_0_0_0_after_this3)<-[:ADMIN_IN]-(authorization_3_0_0_0_after_this4:User) WHERE ($jwt.id IS NOT NULL AND authorization_3_0_0_0_after_this4.userId = $jwt.id) | 1]) > 0) - RETURN count(authorization_3_0_0_0_after_this2) = 1 AS authorization_3_0_0_0_after_var5 + WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_0_0_0_0_0_0_after_this3)<-[:ADMIN_IN]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this4:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_0_0_0_0_0_0_after_this4.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_0_0_0_0_0_0_0_0_0_after_this2) = 1 AS authorization_0_0_0_0_0_0_0_0_0_0_after_var5 } WITH * - WHERE authorization_3_0_0_0_after_var5 = true - RETURN count(authorization_3_0_0_0_after_this1) = 1 AS authorization_3_0_0_0_after_var0 + WHERE authorization_0_0_0_0_0_0_0_0_0_0_after_var5 = true + RETURN count(authorization_0_0_0_0_0_0_0_0_0_0_after_this1) = 1 AS authorization_0_0_0_0_0_0_0_0_0_0_after_var0 } CALL { WITH this0_settings0_node_openingDays0_node - MATCH (this0_settings0_node_openingDays0_node)<-[:VALID_GARAGES]-(authorization_2_0_0_0_after_this1:Settings) - OPTIONAL MATCH (authorization_2_0_0_0_after_this1)<-[:HAS_SETTINGS]-(authorization_2_0_0_0_after_this2:Tenant) - WITH *, count(authorization_2_0_0_0_after_this2) AS tenantCount + MATCH (this0_settings0_node_openingDays0_node)<-[:VALID_GARAGES]-(authorization_0_0_0_0_0_0_0_after_this1:Settings) + OPTIONAL MATCH (authorization_0_0_0_0_0_0_0_after_this1)<-[:HAS_SETTINGS]-(authorization_0_0_0_0_0_0_0_after_this2:Tenant) + WITH *, count(authorization_0_0_0_0_0_0_0_after_this2) AS tenantCount WITH * - WHERE (tenantCount <> 0 AND size([(authorization_2_0_0_0_after_this2)<-[:ADMIN_IN]-(authorization_2_0_0_0_after_this3:User) WHERE ($jwt.id IS NOT NULL AND authorization_2_0_0_0_after_this3.userId = $jwt.id) | 1]) > 0) - RETURN count(authorization_2_0_0_0_after_this1) = 1 AS authorization_2_0_0_0_after_var0 + WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_0_0_0_after_this2)<-[:ADMIN_IN]-(authorization_0_0_0_0_0_0_0_after_this3:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_0_0_0_after_this3.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_0_0_0_0_0_0_after_this1) = 1 AS authorization_0_0_0_0_0_0_0_after_var0 } - OPTIONAL MATCH (this0_settings0_node)<-[:HAS_SETTINGS]-(authorization_1_0_0_0_after_this1:Tenant) - WITH *, count(authorization_1_0_0_0_after_this1) AS tenantCount + OPTIONAL MATCH (this0_settings0_node)<-[:HAS_SETTINGS]-(authorization_0_0_0_0_after_this1:Tenant) + WITH *, count(authorization_0_0_0_0_after_this1) AS tenantCount WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_3_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_2_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (tenantCount <> 0 AND size([(authorization_1_0_0_0_after_this1)<-[:ADMIN_IN]-(authorization_1_0_0_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_1_0_0_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[:ADMIN_IN]-(authorization_0_0_0_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_after_this0.userId = $jwt.id) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_0_0_0_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (tenantCount <> 0 AND size([(authorization_0_0_0_0_after_this1)<-[:ADMIN_IN]-(authorization_0_0_0_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[:ADMIN_IN]-(authorization_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_after_this0.userId = $jwt.id) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this0 } CALL { diff --git a/packages/graphql/tests/tck/issues/4214.test.ts b/packages/graphql/tests/tck/issues/4214.test.ts index 66f4399f149..7149f8076ad 100644 --- a/packages/graphql/tests/tck/issues/4214.test.ts +++ b/packages/graphql/tests/tck/issues/4214.test.ts @@ -168,12 +168,12 @@ describe("https://github.com/neo4j/graphql/issues/4214", () => { CALL { WITH this0 OPTIONAL MATCH (this0_transaction_connect0_node:Transaction) - OPTIONAL MATCH (this0_transaction_connect0_node)-[:TRANSACTION]->(authorization_0_0_0_0_before_this0:Store) - WITH *, count(authorization_0_0_0_0_before_this0) AS storeCount - OPTIONAL MATCH (this0_transaction_connect0_node)-[:TRANSACTION]->(authorization_0_0_0_0_before_this1:Store) - WITH *, count(authorization_0_0_0_0_before_this1) AS storeCount + OPTIONAL MATCH (this0_transaction_connect0_node)-[:TRANSACTION]->(authorization_0_before_this0:Store) + WITH *, count(authorization_0_before_this0) AS storeCount + OPTIONAL MATCH (this0_transaction_connect0_node)-[:TRANSACTION]->(authorization_0_before_this1:Store) + WITH *, count(authorization_0_before_this1) AS storeCount WITH * - WHERE this0_transaction_connect0_node.id = $this0_transaction_connect0_node_param0 AND ((($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_before_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_before_param3 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_before_param4 IN $jwt.roles)) AND (storeCount <> 0 AND ($jwt.store IS NOT NULL AND authorization_0_0_0_0_before_this0.id = $jwt.store)))) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_before_param5 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_before_param6 IN $jwt.roles)) AND (storeCount <> 0 AND ($jwt.store IS NOT NULL AND authorization_0_0_0_0_before_this1.id = $jwt.store))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE this0_transaction_connect0_node.id = $this0_transaction_connect0_node_param0 AND ((($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_before_param2 IN $jwt.roles)) OR ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $authorization_0_before_param3 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $authorization_0_before_param4 IN $jwt.roles)) AND (storeCount <> 0 AND ($jwt.store IS NOT NULL AND authorization_0_before_this0.id = $jwt.store)))) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $authorization_0_before_param5 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $authorization_0_before_param6 IN $jwt.roles)) AND (storeCount <> 0 AND ($jwt.store IS NOT NULL AND authorization_0_before_this1.id = $jwt.store))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) CALL { WITH * WITH collect(this0_transaction_connect0_node) as connectedNodes, collect(this0) as parentNodes @@ -188,17 +188,17 @@ describe("https://github.com/neo4j/graphql/issues/4214", () => { WITH * CALL { WITH this0 - MATCH (this0)-[:ITEM_TRANSACTED]->(authorization_0_0_0_0_after_this2:Transaction) - OPTIONAL MATCH (authorization_0_0_0_0_after_this2)-[:TRANSACTION]->(authorization_0_0_0_0_after_this3:Store) - WITH *, count(authorization_0_0_0_0_after_this3) AS storeCount + MATCH (this0)-[:ITEM_TRANSACTED]->(authorization_0_after_this2:Transaction) + OPTIONAL MATCH (authorization_0_after_this2)-[:TRANSACTION]->(authorization_0_after_this3:Store) + WITH *, count(authorization_0_after_this3) AS storeCount WITH * - WHERE (storeCount <> 0 AND ($jwt.store IS NOT NULL AND authorization_0_0_0_0_after_this3.id = $jwt.store)) - RETURN count(authorization_0_0_0_0_after_this2) = 1 AS authorization_0_0_0_0_after_var0 + WHERE (storeCount <> 0 AND ($jwt.store IS NOT NULL AND authorization_0_after_this3.id = $jwt.store)) + RETURN count(authorization_0_after_this2) = 1 AS authorization_0_after_var0 } - OPTIONAL MATCH (this0_transaction_connect0_node)-[:TRANSACTION]->(authorization_0_0_0_0_after_this1:Store) - WITH *, count(authorization_0_0_0_0_after_this1) AS storeCount + OPTIONAL MATCH (this0_transaction_connect0_node)-[:TRANSACTION]->(authorization_0_after_this1:Store) + WITH *, count(authorization_0_after_this1) AS storeCount WITH * - WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param3 IN $jwt.roles)) AND authorization_0_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param4 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param5 IN $jwt.roles)) AND (storeCount <> 0 AND ($jwt.store IS NOT NULL AND authorization_0_0_0_0_after_this1.id = $jwt.store))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) + WHERE (apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $authorization_0_after_param3 IN $jwt.roles)) AND authorization_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $authorization_0_after_param4 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $authorization_0_after_param5 IN $jwt.roles)) AND (storeCount <> 0 AND ($jwt.store IS NOT NULL AND authorization_0_after_this1.id = $jwt.store))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) RETURN count(*) AS connect_this0_transaction_connect_Transaction0 } WITH * @@ -212,15 +212,15 @@ describe("https://github.com/neo4j/graphql/issues/4214", () => { WITH * CALL { WITH this0 - MATCH (this0)-[:ITEM_TRANSACTED]->(authorization_0_0_0_0_after_this1:Transaction) - OPTIONAL MATCH (authorization_0_0_0_0_after_this1)-[:TRANSACTION]->(authorization_0_0_0_0_after_this2:Store) - WITH *, count(authorization_0_0_0_0_after_this2) AS storeCount + MATCH (this0)-[:ITEM_TRANSACTED]->(authorization_0_after_this1:Transaction) + OPTIONAL MATCH (authorization_0_after_this1)-[:TRANSACTION]->(authorization_0_after_this2:Store) + WITH *, count(authorization_0_after_this2) AS storeCount WITH * - WHERE (storeCount <> 0 AND ($jwt.store IS NOT NULL AND authorization_0_0_0_0_after_this2.id = $jwt.store)) - RETURN count(authorization_0_0_0_0_after_this1) = 1 AS authorization_0_0_0_0_after_var0 + WHERE (storeCount <> 0 AND ($jwt.store IS NOT NULL AND authorization_0_after_this2.id = $jwt.store)) + RETURN count(authorization_0_after_this1) = 1 AS authorization_0_after_var0 } WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param3 IN $jwt.roles)) AND authorization_0_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles) OR ($jwt.roles IS NOT NULL AND $authorization_0_after_param3 IN $jwt.roles)) AND authorization_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this0 } CALL { @@ -265,15 +265,15 @@ describe("https://github.com/neo4j/graphql/issues/4214", () => { \\"high\\": 0 }, \\"this0_transaction_connect0_node_param0\\": \\"transactionid\\", - \\"authorization_0_0_0_0_before_param2\\": \\"admin\\", - \\"authorization_0_0_0_0_before_param3\\": \\"store-owner\\", - \\"authorization_0_0_0_0_before_param4\\": \\"employee\\", - \\"authorization_0_0_0_0_before_param5\\": \\"store-owner\\", - \\"authorization_0_0_0_0_before_param6\\": \\"employee\\", - \\"authorization_0_0_0_0_after_param2\\": \\"store-owner\\", - \\"authorization_0_0_0_0_after_param3\\": \\"employee\\", - \\"authorization_0_0_0_0_after_param4\\": \\"store-owner\\", - \\"authorization_0_0_0_0_after_param5\\": \\"employee\\", + \\"authorization_0_before_param2\\": \\"admin\\", + \\"authorization_0_before_param3\\": \\"store-owner\\", + \\"authorization_0_before_param4\\": \\"employee\\", + \\"authorization_0_before_param5\\": \\"store-owner\\", + \\"authorization_0_before_param6\\": \\"employee\\", + \\"authorization_0_after_param2\\": \\"store-owner\\", + \\"authorization_0_after_param3\\": \\"employee\\", + \\"authorization_0_after_param4\\": \\"store-owner\\", + \\"authorization_0_after_param5\\": \\"employee\\", \\"resolvedCallbacks\\": {} }" `); diff --git a/packages/graphql/tests/tck/issues/4223.test.ts b/packages/graphql/tests/tck/issues/4223.test.ts index babf4cfc3b2..093ad5a2d78 100644 --- a/packages/graphql/tests/tck/issues/4223.test.ts +++ b/packages/graphql/tests/tck/issues/4223.test.ts @@ -219,42 +219,42 @@ describe("https://github.com/neo4j/graphql/issues/4223", () => { WITH * CALL { WITH this0_settings0_node_openingDays0_node_open0_node - MATCH (this0_settings0_node_openingDays0_node_open0_node)<-[:HAS_OPEN_INTERVALS]-(authorization_3_0_0_0_after_this1:OpeningDay) + MATCH (this0_settings0_node_openingDays0_node_open0_node)<-[:HAS_OPEN_INTERVALS]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this1:OpeningDay) CALL { - WITH authorization_3_0_0_0_after_this1 - MATCH (authorization_3_0_0_0_after_this1)<-[:VALID_GARAGES]-(authorization_3_0_0_0_after_this2:Settings) - OPTIONAL MATCH (authorization_3_0_0_0_after_this2)<-[:HAS_SETTINGS]-(authorization_3_0_0_0_after_this3:Tenant) - WITH *, count(authorization_3_0_0_0_after_this3) AS tenantCount + WITH authorization_0_0_0_0_0_0_0_0_0_0_after_this1 + MATCH (authorization_0_0_0_0_0_0_0_0_0_0_after_this1)<-[:VALID_GARAGES]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this2:Settings) + OPTIONAL MATCH (authorization_0_0_0_0_0_0_0_0_0_0_after_this2)<-[:HAS_SETTINGS]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this3:Tenant) + WITH *, count(authorization_0_0_0_0_0_0_0_0_0_0_after_this3) AS tenantCount WITH * - WHERE (tenantCount <> 0 AND size([(authorization_3_0_0_0_after_this3)<-[:ADMIN_IN]-(authorization_3_0_0_0_after_this4:User) WHERE ($jwt.id IS NOT NULL AND authorization_3_0_0_0_after_this4.userId = $jwt.id) | 1]) > 0) - RETURN count(authorization_3_0_0_0_after_this2) = 1 AS authorization_3_0_0_0_after_var5 + WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_0_0_0_0_0_0_after_this3)<-[:ADMIN_IN]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this4:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_0_0_0_0_0_0_after_this4.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_0_0_0_0_0_0_0_0_0_after_this2) = 1 AS authorization_0_0_0_0_0_0_0_0_0_0_after_var5 } WITH * - WHERE authorization_3_0_0_0_after_var5 = true - RETURN count(authorization_3_0_0_0_after_this1) = 1 AS authorization_3_0_0_0_after_var0 + WHERE authorization_0_0_0_0_0_0_0_0_0_0_after_var5 = true + RETURN count(authorization_0_0_0_0_0_0_0_0_0_0_after_this1) = 1 AS authorization_0_0_0_0_0_0_0_0_0_0_after_var0 } CALL { WITH this0_settings0_node_openingDays0_node - MATCH (this0_settings0_node_openingDays0_node)<-[:VALID_GARAGES]-(authorization_2_0_0_0_after_this1:Settings) - OPTIONAL MATCH (authorization_2_0_0_0_after_this1)<-[:HAS_SETTINGS]-(authorization_2_0_0_0_after_this2:Tenant) - WITH *, count(authorization_2_0_0_0_after_this2) AS tenantCount + MATCH (this0_settings0_node_openingDays0_node)<-[:VALID_GARAGES]-(authorization_0_0_0_0_0_0_0_after_this1:Settings) + OPTIONAL MATCH (authorization_0_0_0_0_0_0_0_after_this1)<-[:HAS_SETTINGS]-(authorization_0_0_0_0_0_0_0_after_this2:Tenant) + WITH *, count(authorization_0_0_0_0_0_0_0_after_this2) AS tenantCount WITH * - WHERE (tenantCount <> 0 AND size([(authorization_2_0_0_0_after_this2)<-[:ADMIN_IN]-(authorization_2_0_0_0_after_this3:User) WHERE ($jwt.id IS NOT NULL AND authorization_2_0_0_0_after_this3.userId = $jwt.id) | 1]) > 0) - RETURN count(authorization_2_0_0_0_after_this1) = 1 AS authorization_2_0_0_0_after_var0 + WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_0_0_0_after_this2)<-[:ADMIN_IN]-(authorization_0_0_0_0_0_0_0_after_this3:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_0_0_0_after_this3.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_0_0_0_0_0_0_after_this1) = 1 AS authorization_0_0_0_0_0_0_0_after_var0 } CALL { WITH this0_settings0_node_myWorkspace0_node - MATCH (this0_settings0_node_myWorkspace0_node)<-[:HAS_WORKSPACE_SETTINGS]-(authorization_2_1_0_0_after_this1:Settings) - OPTIONAL MATCH (authorization_2_1_0_0_after_this1)<-[:HAS_SETTINGS]-(authorization_2_1_0_0_after_this2:Tenant) - WITH *, count(authorization_2_1_0_0_after_this2) AS tenantCount + MATCH (this0_settings0_node_myWorkspace0_node)<-[:HAS_WORKSPACE_SETTINGS]-(authorization_0_0_0_0_1_0_0_after_this1:Settings) + OPTIONAL MATCH (authorization_0_0_0_0_1_0_0_after_this1)<-[:HAS_SETTINGS]-(authorization_0_0_0_0_1_0_0_after_this2:Tenant) + WITH *, count(authorization_0_0_0_0_1_0_0_after_this2) AS tenantCount WITH * - WHERE (tenantCount <> 0 AND size([(authorization_2_1_0_0_after_this2)<-[:ADMIN_IN]-(authorization_2_1_0_0_after_this3:User) WHERE ($jwt.id IS NOT NULL AND authorization_2_1_0_0_after_this3.userId = $jwt.id) | 1]) > 0) - RETURN count(authorization_2_1_0_0_after_this1) = 1 AS authorization_2_1_0_0_after_var0 + WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_1_0_0_after_this2)<-[:ADMIN_IN]-(authorization_0_0_0_0_1_0_0_after_this3:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_1_0_0_after_this3.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_0_0_0_1_0_0_after_this1) = 1 AS authorization_0_0_0_0_1_0_0_after_var0 } - OPTIONAL MATCH (this0_settings0_node)<-[:HAS_SETTINGS]-(authorization_1_0_0_0_after_this1:Tenant) - WITH *, count(authorization_1_0_0_0_after_this1) AS tenantCount + OPTIONAL MATCH (this0_settings0_node)<-[:HAS_SETTINGS]-(authorization_0_0_0_0_after_this1:Tenant) + WITH *, count(authorization_0_0_0_0_after_this1) AS tenantCount WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_3_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_2_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_2_1_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (tenantCount <> 0 AND size([(authorization_1_0_0_0_after_this1)<-[:ADMIN_IN]-(authorization_1_0_0_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_1_0_0_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[:ADMIN_IN]-(authorization_0_0_0_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_after_this0.userId = $jwt.id) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_0_0_0_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_1_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (tenantCount <> 0 AND size([(authorization_0_0_0_0_after_this1)<-[:ADMIN_IN]-(authorization_0_0_0_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[:ADMIN_IN]-(authorization_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_after_this0.userId = $jwt.id) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this0 } CALL { diff --git a/packages/graphql/tests/tck/issues/4292.test.ts b/packages/graphql/tests/tck/issues/4292.test.ts index 451365dc788..63a37314dac 100644 --- a/packages/graphql/tests/tck/issues/4292.test.ts +++ b/packages/graphql/tests/tck/issues/4292.test.ts @@ -248,15 +248,20 @@ describe("https://github.com/neo4j/graphql/issues/4292", () => { } WITH * WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ((creatorCount <> 0 AND ($jwt.uid IS NOT NULL AND this14.id = $jwt.uid)) OR (groupCount <> 0 AND size([(this15)<-[:ADMIN_OF]-(this22:Admin) WHERE single(this21 IN [(this22)-[:IS_USER]->(this21:User) WHERE ($jwt.uid IS NOT NULL AND this21.id = $jwt.uid) | 1] WHERE true) | 1]) > 0) OR (groupCount <> 0 AND size([(this16)<-[:CONTRIBUTOR_TO]-(this24:Contributor) WHERE single(this23 IN [(this24)-[:IS_USER]->(this23:User) WHERE ($jwt.uid IS NOT NULL AND this23.id = $jwt.uid) | 1] WHERE true) | 1]) > 0) OR var20 = true)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { active: this12.active, firstDay: this12.firstDay, lastDay: this12.lastDay, node: { __resolveType: \\"Person\\", __id: id(this13) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this13, relationship: this12 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var25 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this13, edge.relationship AS this12 + RETURN collect({ active: this12.active, firstDay: this12.firstDay, lastDay: this12.lastDay, node: { __resolveType: \\"Person\\", __id: id(this13) } }) AS var25 + } + RETURN { edges: var25, totalCount: totalCount } AS var26 } - WITH this1 { .id, .name, partnersConnection: var25 } AS this1 - RETURN collect(this1) AS var26 + WITH this1 { .id, .name, partnersConnection: var26 } AS this1 + RETURN collect(this1) AS var27 } - RETURN this { .id, .name, members: var26 } AS this" + RETURN this { .id, .name, members: var27 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/issues/433.test.ts b/packages/graphql/tests/tck/issues/433.test.ts index 8944a8950d4..d5215801332 100644 --- a/packages/graphql/tests/tck/issues/433.test.ts +++ b/packages/graphql/tests/tck/issues/433.test.ts @@ -67,12 +67,17 @@ describe("#413", () => { CALL { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Person) - WITH { node: { name: this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { name: this1.name } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { .title, actorsConnection: var2 } AS this" + RETURN this { .title, actorsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); diff --git a/packages/graphql/tests/tck/issues/4405.test.ts b/packages/graphql/tests/tck/issues/4405.test.ts new file mode 100644 index 00000000000..98fc4e5dd26 --- /dev/null +++ b/packages/graphql/tests/tck/issues/4405.test.ts @@ -0,0 +1,139 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import gql from "graphql-tag"; +import { Neo4jGraphQL } from "../../../src"; +import { createBearerToken } from "../../utils/create-bearer-token"; +import { translateQuery, formatCypher, formatParams } from "../utils/tck-test-utils"; + +describe("https://github.com/neo4j/graphql/issues/4405", () => { + test("authorization should work when the filter value is an array", async () => { + const typeDefs = /* GraphQL */ ` + type Movie { + title: String + } + + type Actor + @authorization( + validate: [ + { + when: [BEFORE] + operations: [READ] + where: { node: { actedInConnection_SOME: { node: { title_IN: ["Matrix"] } } } } + } + ] + ) { + name: String! + actedIn: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs, features: { authorization: { key: "secret" } } }); + + const query = gql` + query actors { + actors { + name + } + } + `; + + const token = createBearerToken("secret", { roles: ["admin"], id: "something", email: "something" }); + const result = await translateQuery(neoSchema, query, { token }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this)-[this1:ACTED_IN]->(this0:Movie) WHERE ($param1 IS NOT NULL AND this0.title IN $param1) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .name } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"isAuthenticated\\": true, + \\"param1\\": [ + \\"Matrix\\" + ] + }" + `); + }); + + test("authorization should work when the filter value is an array, inside logical", async () => { + const typeDefs = /* GraphQL */ ` + type Movie { + title: String + } + + type Actor + @authorization( + validate: [ + { + when: [BEFORE] + operations: [READ] + where: { + node: { + actedInConnection_SOME: { + node: { + OR: [{ title_IN: ["Matrix"] }, { title_IN: ["Forrest Gump", "Top Gun"] }] + } + } + } + } + } + ] + ) { + name: String! + actedIn: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs, features: { authorization: { key: "secret" } } }); + + const query = gql` + query actors { + actors { + name + } + } + `; + + const token = createBearerToken("secret", { roles: ["admin"], id: "something", email: "something" }); + const result = await translateQuery(neoSchema, query, { token }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this)-[this1:ACTED_IN]->(this0:Movie) WHERE (($param1 IS NOT NULL AND this0.title IN $param1) OR ($param2 IS NOT NULL AND this0.title IN $param2)) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this { .name } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"isAuthenticated\\": true, + \\"param1\\": [ + \\"Matrix\\" + ], + \\"param2\\": [ + \\"Forrest Gump\\", + \\"Top Gun\\" + ] + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/issues/4429.test.ts b/packages/graphql/tests/tck/issues/4429.test.ts new file mode 100644 index 00000000000..d1f80c8e450 --- /dev/null +++ b/packages/graphql/tests/tck/issues/4429.test.ts @@ -0,0 +1,399 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { gql } from "graphql-tag"; +import { Neo4jGraphQL } from "../../../src"; +import { formatCypher, translateQuery, formatParams } from "../utils/tck-test-utils"; + +describe("https://github.com/neo4j/graphql/issues/4429", () => { + let neoSchema: Neo4jGraphQL; + + const typeDefs = gql` + type JWT @jwt { + id: String + roles: [String] + } + type User @authorization(validate: [{ where: { node: { userId: "$jwt.id" } }, operations: [READ] }]) { + userId: String! @unique + adminAccess: [Tenant!]! @relationship(type: "ADMIN_IN", direction: OUT) + } + + type Tenant @authorization(validate: [{ where: { node: { admins: { userId: "$jwt.id" } } } }]) { + id: ID! @id + admins: [User!]! @relationship(type: "ADMIN_IN", direction: IN) + settings: Settings @relationship(type: "VEHICLECARD_OWNER", direction: IN) + } + + type Settings @authorization(validate: [{ where: { node: { tenant: { admins: { userId: "$jwt.id" } } } } }]) { + id: ID! @id + openingDays: [OpeningDay!]! @relationship(type: "VALID_GARAGES", direction: OUT) + tenant: Tenant! @relationship(type: "VEHICLECARD_OWNER", direction: OUT) # <--- this line + } + + type OpeningDay + @authorization( + validate: [{ where: { node: { settings: { tenant: { admins: { userId: "$jwt.id" } } } } } }] + ) { + id: ID! @id + settings: Settings @relationship(type: "VALID_GARAGES", direction: IN) + open: [OpeningHoursInterval!]! @relationship(type: "HAS_OPEN_INTERVALS", direction: OUT) + } + type OpeningHoursInterval + @authorization( + validate: [ + { where: { node: { openingDay: { settings: { tenant: { admins: { userId: "$jwt.id" } } } } } } } + ] + ) { + name: String + openingDay: OpeningDay! @relationship(type: "HAS_OPEN_INTERVALS", direction: IN) + createdAt: DateTime! @timestamp(operations: [CREATE]) + updatedAt: DateTime! @timestamp(operations: [CREATE, UPDATE]) + updatedBy: String @populatedBy(callback: "getUserIDFromContext", operations: [CREATE, UPDATE]) + } + `; + + beforeAll(() => { + neoSchema = new Neo4jGraphQL({ + typeDefs, + features: { + populatedBy: { + callbacks: { + getUserIDFromContext: () => "hi", + }, + }, + }, + }); + }); + + test("should include checks for auth jwt param is not null", async () => { + const query = gql` + mutation addTenant($input: [TenantCreateInput!]!) { + createTenants(input: $input) { + tenants { + id + admins { + userId + } + settings { + openingDays { + open { + name + } + } + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query, { + variableValues: { + input: { + settings: { + create: { + node: { + openingDays: { + create: [ + { + node: { + open: { + create: [ + { + node: { + name: "hi", + }, + }, + ], + }, + }, + }, + { + node: { + open: { + create: [ + { + node: { + name: "hi", + }, + }, + { + node: { + name: "hi", + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + }, + }, + }, + }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL { + CREATE (this0:Tenant) + SET this0.id = randomUUID() + WITH * + CREATE (this0_settings0_node:Settings) + SET this0_settings0_node.id = randomUUID() + WITH * + CREATE (this0_settings0_node_openingDays0_node:OpeningDay) + SET this0_settings0_node_openingDays0_node.id = randomUUID() + WITH * + CREATE (this0_settings0_node_openingDays0_node_open0_node:OpeningHoursInterval) + SET this0_settings0_node_openingDays0_node_open0_node.createdAt = datetime() + SET this0_settings0_node_openingDays0_node_open0_node.updatedAt = datetime() + SET this0_settings0_node_openingDays0_node_open0_node.updatedBy = $resolvedCallbacks.this0_settings0_node_openingDays0_node_open0_node_updatedBy_getUserIDFromContext + SET this0_settings0_node_openingDays0_node_open0_node.name = $this0_settings0_node_openingDays0_node_open0_node_name + MERGE (this0_settings0_node_openingDays0_node)-[:HAS_OPEN_INTERVALS]->(this0_settings0_node_openingDays0_node_open0_node) + WITH * + CALL { + WITH this0_settings0_node_openingDays0_node_open0_node + MATCH (this0_settings0_node_openingDays0_node_open0_node)<-[this0_settings0_node_openingDays0_node_open0_node_openingDay_OpeningDay_unique:HAS_OPEN_INTERVALS]-(:OpeningDay) + WITH count(this0_settings0_node_openingDays0_node_open0_node_openingDay_OpeningDay_unique) as c + WHERE apoc.util.validatePredicate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDOpeningHoursInterval.openingDay required exactly once', [0]) + RETURN c AS this0_settings0_node_openingDays0_node_open0_node_openingDay_OpeningDay_unique_ignored + } + MERGE (this0_settings0_node)-[:VALID_GARAGES]->(this0_settings0_node_openingDays0_node) + WITH * + CALL { + WITH this0_settings0_node_openingDays0_node + MATCH (this0_settings0_node_openingDays0_node)<-[this0_settings0_node_openingDays0_node_settings_Settings_unique:VALID_GARAGES]-(:Settings) + WITH count(this0_settings0_node_openingDays0_node_settings_Settings_unique) as c + WHERE apoc.util.validatePredicate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDOpeningDay.settings must be less than or equal to one', [0]) + RETURN c AS this0_settings0_node_openingDays0_node_settings_Settings_unique_ignored + } + WITH * + CREATE (this0_settings0_node_openingDays1_node:OpeningDay) + SET this0_settings0_node_openingDays1_node.id = randomUUID() + WITH * + CREATE (this0_settings0_node_openingDays1_node_open0_node:OpeningHoursInterval) + SET this0_settings0_node_openingDays1_node_open0_node.createdAt = datetime() + SET this0_settings0_node_openingDays1_node_open0_node.updatedAt = datetime() + SET this0_settings0_node_openingDays1_node_open0_node.updatedBy = $resolvedCallbacks.this0_settings0_node_openingDays1_node_open0_node_updatedBy_getUserIDFromContext + SET this0_settings0_node_openingDays1_node_open0_node.name = $this0_settings0_node_openingDays1_node_open0_node_name + MERGE (this0_settings0_node_openingDays1_node)-[:HAS_OPEN_INTERVALS]->(this0_settings0_node_openingDays1_node_open0_node) + WITH * + CALL { + WITH this0_settings0_node_openingDays1_node_open0_node + MATCH (this0_settings0_node_openingDays1_node_open0_node)<-[this0_settings0_node_openingDays1_node_open0_node_openingDay_OpeningDay_unique:HAS_OPEN_INTERVALS]-(:OpeningDay) + WITH count(this0_settings0_node_openingDays1_node_open0_node_openingDay_OpeningDay_unique) as c + WHERE apoc.util.validatePredicate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDOpeningHoursInterval.openingDay required exactly once', [0]) + RETURN c AS this0_settings0_node_openingDays1_node_open0_node_openingDay_OpeningDay_unique_ignored + } + WITH * + CREATE (this0_settings0_node_openingDays1_node_open1_node:OpeningHoursInterval) + SET this0_settings0_node_openingDays1_node_open1_node.createdAt = datetime() + SET this0_settings0_node_openingDays1_node_open1_node.updatedAt = datetime() + SET this0_settings0_node_openingDays1_node_open1_node.updatedBy = $resolvedCallbacks.this0_settings0_node_openingDays1_node_open1_node_updatedBy_getUserIDFromContext + SET this0_settings0_node_openingDays1_node_open1_node.name = $this0_settings0_node_openingDays1_node_open1_node_name + MERGE (this0_settings0_node_openingDays1_node)-[:HAS_OPEN_INTERVALS]->(this0_settings0_node_openingDays1_node_open1_node) + WITH * + CALL { + WITH this0_settings0_node_openingDays1_node_open1_node + MATCH (this0_settings0_node_openingDays1_node_open1_node)<-[this0_settings0_node_openingDays1_node_open1_node_openingDay_OpeningDay_unique:HAS_OPEN_INTERVALS]-(:OpeningDay) + WITH count(this0_settings0_node_openingDays1_node_open1_node_openingDay_OpeningDay_unique) as c + WHERE apoc.util.validatePredicate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDOpeningHoursInterval.openingDay required exactly once', [0]) + RETURN c AS this0_settings0_node_openingDays1_node_open1_node_openingDay_OpeningDay_unique_ignored + } + MERGE (this0_settings0_node)-[:VALID_GARAGES]->(this0_settings0_node_openingDays1_node) + WITH * + CALL { + WITH this0_settings0_node_openingDays1_node + MATCH (this0_settings0_node_openingDays1_node)<-[this0_settings0_node_openingDays1_node_settings_Settings_unique:VALID_GARAGES]-(:Settings) + WITH count(this0_settings0_node_openingDays1_node_settings_Settings_unique) as c + WHERE apoc.util.validatePredicate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDOpeningDay.settings must be less than or equal to one', [0]) + RETURN c AS this0_settings0_node_openingDays1_node_settings_Settings_unique_ignored + } + MERGE (this0)<-[:VEHICLECARD_OWNER]-(this0_settings0_node) + WITH * + CALL { + WITH this0_settings0_node + MATCH (this0_settings0_node)-[this0_settings0_node_tenant_Tenant_unique:VEHICLECARD_OWNER]->(:Tenant) + WITH count(this0_settings0_node_tenant_Tenant_unique) as c + WHERE apoc.util.validatePredicate(NOT (c = 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDSettings.tenant required exactly once', [0]) + RETURN c AS this0_settings0_node_tenant_Tenant_unique_ignored + } + WITH * + CALL { + WITH this0 + MATCH (this0)<-[this0_settings_Settings_unique:VEHICLECARD_OWNER]-(:Settings) + WITH count(this0_settings_Settings_unique) as c + WHERE apoc.util.validatePredicate(NOT (c <= 1), '@neo4j/graphql/RELATIONSHIP-REQUIREDTenant.settings must be less than or equal to one', [0]) + RETURN c AS this0_settings_Settings_unique_ignored + } + WITH * + CALL { + WITH this0_settings0_node_openingDays0_node_open0_node + MATCH (this0_settings0_node_openingDays0_node_open0_node)<-[:HAS_OPEN_INTERVALS]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this1:OpeningDay) + CALL { + WITH authorization_0_0_0_0_0_0_0_0_0_0_after_this1 + MATCH (authorization_0_0_0_0_0_0_0_0_0_0_after_this1)<-[:VALID_GARAGES]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this2:Settings) + OPTIONAL MATCH (authorization_0_0_0_0_0_0_0_0_0_0_after_this2)-[:VEHICLECARD_OWNER]->(authorization_0_0_0_0_0_0_0_0_0_0_after_this3:Tenant) + WITH *, count(authorization_0_0_0_0_0_0_0_0_0_0_after_this3) AS tenantCount + WITH * + WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_0_0_0_0_0_0_after_this3)<-[:ADMIN_IN]-(authorization_0_0_0_0_0_0_0_0_0_0_after_this4:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_0_0_0_0_0_0_after_this4.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_0_0_0_0_0_0_0_0_0_after_this2) = 1 AS authorization_0_0_0_0_0_0_0_0_0_0_after_var5 + } + WITH * + WHERE authorization_0_0_0_0_0_0_0_0_0_0_after_var5 = true + RETURN count(authorization_0_0_0_0_0_0_0_0_0_0_after_this1) = 1 AS authorization_0_0_0_0_0_0_0_0_0_0_after_var0 + } + CALL { + WITH this0_settings0_node_openingDays0_node + MATCH (this0_settings0_node_openingDays0_node)<-[:VALID_GARAGES]-(authorization_0_0_0_0_0_0_0_after_this1:Settings) + OPTIONAL MATCH (authorization_0_0_0_0_0_0_0_after_this1)-[:VEHICLECARD_OWNER]->(authorization_0_0_0_0_0_0_0_after_this2:Tenant) + WITH *, count(authorization_0_0_0_0_0_0_0_after_this2) AS tenantCount + WITH * + WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_0_0_0_after_this2)<-[:ADMIN_IN]-(authorization_0_0_0_0_0_0_0_after_this3:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_0_0_0_after_this3.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_0_0_0_0_0_0_after_this1) = 1 AS authorization_0_0_0_0_0_0_0_after_var0 + } + CALL { + WITH this0_settings0_node_openingDays1_node_open0_node + MATCH (this0_settings0_node_openingDays1_node_open0_node)<-[:HAS_OPEN_INTERVALS]-(authorization_0_0_0_0_0_1_0_0_0_0_after_this1:OpeningDay) + CALL { + WITH authorization_0_0_0_0_0_1_0_0_0_0_after_this1 + MATCH (authorization_0_0_0_0_0_1_0_0_0_0_after_this1)<-[:VALID_GARAGES]-(authorization_0_0_0_0_0_1_0_0_0_0_after_this2:Settings) + OPTIONAL MATCH (authorization_0_0_0_0_0_1_0_0_0_0_after_this2)-[:VEHICLECARD_OWNER]->(authorization_0_0_0_0_0_1_0_0_0_0_after_this3:Tenant) + WITH *, count(authorization_0_0_0_0_0_1_0_0_0_0_after_this3) AS tenantCount + WITH * + WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_0_1_0_0_0_0_after_this3)<-[:ADMIN_IN]-(authorization_0_0_0_0_0_1_0_0_0_0_after_this4:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_0_1_0_0_0_0_after_this4.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_0_0_0_0_1_0_0_0_0_after_this2) = 1 AS authorization_0_0_0_0_0_1_0_0_0_0_after_var5 + } + WITH * + WHERE authorization_0_0_0_0_0_1_0_0_0_0_after_var5 = true + RETURN count(authorization_0_0_0_0_0_1_0_0_0_0_after_this1) = 1 AS authorization_0_0_0_0_0_1_0_0_0_0_after_var0 + } + CALL { + WITH this0_settings0_node_openingDays1_node_open1_node + MATCH (this0_settings0_node_openingDays1_node_open1_node)<-[:HAS_OPEN_INTERVALS]-(authorization_0_0_0_0_0_1_0_0_1_0_after_this1:OpeningDay) + CALL { + WITH authorization_0_0_0_0_0_1_0_0_1_0_after_this1 + MATCH (authorization_0_0_0_0_0_1_0_0_1_0_after_this1)<-[:VALID_GARAGES]-(authorization_0_0_0_0_0_1_0_0_1_0_after_this2:Settings) + OPTIONAL MATCH (authorization_0_0_0_0_0_1_0_0_1_0_after_this2)-[:VEHICLECARD_OWNER]->(authorization_0_0_0_0_0_1_0_0_1_0_after_this3:Tenant) + WITH *, count(authorization_0_0_0_0_0_1_0_0_1_0_after_this3) AS tenantCount + WITH * + WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_0_1_0_0_1_0_after_this3)<-[:ADMIN_IN]-(authorization_0_0_0_0_0_1_0_0_1_0_after_this4:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_0_1_0_0_1_0_after_this4.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_0_0_0_0_1_0_0_1_0_after_this2) = 1 AS authorization_0_0_0_0_0_1_0_0_1_0_after_var5 + } + WITH * + WHERE authorization_0_0_0_0_0_1_0_0_1_0_after_var5 = true + RETURN count(authorization_0_0_0_0_0_1_0_0_1_0_after_this1) = 1 AS authorization_0_0_0_0_0_1_0_0_1_0_after_var0 + } + CALL { + WITH this0_settings0_node_openingDays1_node + MATCH (this0_settings0_node_openingDays1_node)<-[:VALID_GARAGES]-(authorization_0_0_0_0_0_1_0_after_this1:Settings) + OPTIONAL MATCH (authorization_0_0_0_0_0_1_0_after_this1)-[:VEHICLECARD_OWNER]->(authorization_0_0_0_0_0_1_0_after_this2:Tenant) + WITH *, count(authorization_0_0_0_0_0_1_0_after_this2) AS tenantCount + WITH * + WHERE (tenantCount <> 0 AND size([(authorization_0_0_0_0_0_1_0_after_this2)<-[:ADMIN_IN]-(authorization_0_0_0_0_0_1_0_after_this3:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_0_1_0_after_this3.userId = $jwt.id) | 1]) > 0) + RETURN count(authorization_0_0_0_0_0_1_0_after_this1) = 1 AS authorization_0_0_0_0_0_1_0_after_var0 + } + OPTIONAL MATCH (this0_settings0_node)-[:VEHICLECARD_OWNER]->(authorization_0_0_0_0_after_this1:Tenant) + WITH *, count(authorization_0_0_0_0_after_this1) AS tenantCount + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_0_0_0_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_0_1_0_0_0_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_0_1_0_0_1_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND authorization_0_0_0_0_0_1_0_after_var0 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (tenantCount <> 0 AND size([(authorization_0_0_0_0_after_this1)<-[:ADMIN_IN]-(authorization_0_0_0_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_0_0_0_after_this0.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[:ADMIN_IN]-(authorization_0_after_this0:User) WHERE ($jwt.id IS NOT NULL AND authorization_0_after_this0.userId = $jwt.id) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 + } + CALL { + WITH this0 + CALL { + WITH this0 + MATCH (this0)<-[create_this0:ADMIN_IN]-(create_this1:User) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.id IS NOT NULL AND create_this1.userId = $jwt.id)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH create_this1 { .userId } AS create_this1 + RETURN collect(create_this1) AS create_var2 + } + CALL { + WITH this0 + MATCH (this0)<-[create_this3:VEHICLECARD_OWNER]-(create_this4:Settings) + OPTIONAL MATCH (create_this4)-[:VEHICLECARD_OWNER]->(create_this5:Tenant) + WITH *, count(create_this5) AS tenantCount + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (tenantCount <> 0 AND size([(create_this5)<-[:ADMIN_IN]-(create_this6:User) WHERE ($jwt.id IS NOT NULL AND create_this6.userId = $jwt.id) | 1]) > 0)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH create_this4 + MATCH (create_this4)-[create_this7:VALID_GARAGES]->(create_this8:OpeningDay) + CALL { + WITH create_this8 + MATCH (create_this8)<-[:VALID_GARAGES]-(create_this9:Settings) + OPTIONAL MATCH (create_this9)-[:VEHICLECARD_OWNER]->(create_this10:Tenant) + WITH *, count(create_this10) AS tenantCount + WITH * + WHERE (tenantCount <> 0 AND size([(create_this10)<-[:ADMIN_IN]-(create_this11:User) WHERE ($jwt.id IS NOT NULL AND create_this11.userId = $jwt.id) | 1]) > 0) + RETURN count(create_this9) = 1 AS create_var12 + } + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND create_var12 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH create_this8 + MATCH (create_this8)-[create_this13:HAS_OPEN_INTERVALS]->(create_this14:OpeningHoursInterval) + CALL { + WITH create_this14 + MATCH (create_this14)<-[:HAS_OPEN_INTERVALS]-(create_this15:OpeningDay) + CALL { + WITH create_this15 + MATCH (create_this15)<-[:VALID_GARAGES]-(create_this16:Settings) + OPTIONAL MATCH (create_this16)-[:VEHICLECARD_OWNER]->(create_this17:Tenant) + WITH *, count(create_this17) AS tenantCount + WITH * + WHERE (tenantCount <> 0 AND size([(create_this17)<-[:ADMIN_IN]-(create_this18:User) WHERE ($jwt.id IS NOT NULL AND create_this18.userId = $jwt.id) | 1]) > 0) + RETURN count(create_this16) = 1 AS create_var19 + } + WITH * + WHERE create_var19 = true + RETURN count(create_this15) = 1 AS create_var20 + } + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND create_var20 = true), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WITH create_this14 { .name } AS create_this14 + RETURN collect(create_this14) AS create_var21 + } + WITH create_this8 { open: create_var21 } AS create_this8 + RETURN collect(create_this8) AS create_var22 + } + WITH create_this4 { openingDays: create_var22 } AS create_this4 + RETURN head(collect(create_this4)) AS create_var23 + } + RETURN this0 { .id, admins: create_var2, settings: create_var23 } AS create_var24 + } + RETURN [create_var24] AS data" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"isAuthenticated\\": false, + \\"jwt\\": {}, + \\"this0_settings0_node_openingDays0_node_open0_node_name\\": \\"hi\\", + \\"this0_settings0_node_openingDays1_node_open0_node_name\\": \\"hi\\", + \\"this0_settings0_node_openingDays1_node_open1_node_name\\": \\"hi\\", + \\"resolvedCallbacks\\": { + \\"this0_settings0_node_openingDays0_node_open0_node_updatedBy_getUserIDFromContext\\": \\"hi\\", + \\"this0_settings0_node_openingDays1_node_open0_node_updatedBy_getUserIDFromContext\\": \\"hi\\", + \\"this0_settings0_node_openingDays1_node_open1_node_updatedBy_getUserIDFromContext\\": \\"hi\\" + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/issues/4450.test.ts b/packages/graphql/tests/tck/issues/4450.test.ts new file mode 100644 index 00000000000..14d0657c675 --- /dev/null +++ b/packages/graphql/tests/tck/issues/4450.test.ts @@ -0,0 +1,83 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import gql from "graphql-tag"; +import { Neo4jGraphQL } from "../../../src"; +import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; + +describe("https://github.com/neo4j/graphql/issues/4450", () => { + test("filtering through a connection to a many-to-1 relationship should work", async () => { + const typeDefs = /* GraphQL */ ` + type Actor { + name: String + scene: [Scene!]! @relationship(type: "IN_SCENE", properties: "ActorScene", direction: OUT) + } + + type Scene { + number: Int + actors: [Actor!]! @relationship(type: "IN_SCENE", properties: "ActorScene", direction: IN) + location: Location! @relationship(type: "AT_LOCATION", direction: OUT) + } + + type Location { + city: String + scenes: [Scene!]! @relationship(type: "AT_LOCATION", direction: IN) + } + + interface ActorScene @relationshipProperties { + cut: Boolean + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const query = gql` + query { + actors(where: { sceneConnection_SOME: { edge: { cut: true }, node: { location: { city: "test" } } } }) { + name + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + CALL { + WITH this + MATCH (this)-[this0:IN_SCENE]->(this1:Scene) + OPTIONAL MATCH (this1)-[:AT_LOCATION]->(this2:Location) + WITH *, count(this2) AS locationCount + WITH * + WHERE ((locationCount <> 0 AND this2.city = $param0) AND this0.cut = $param1) + RETURN count(this1) > 0 AS var3 + } + WITH * + WHERE var3 = true + RETURN this { .name } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"test\\", + \\"param1\\": true + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/issues/4477.test.ts b/packages/graphql/tests/tck/issues/4477.test.ts new file mode 100644 index 00000000000..898ed642478 --- /dev/null +++ b/packages/graphql/tests/tck/issues/4477.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import gql from "graphql-tag"; +import { Neo4jGraphQL } from "../../../src"; +import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; + +describe("https://github.com/neo4j/graphql/issues/4477", () => { + test("filtering by count on an aggregate should work", async () => { + const typeDefs = /* GraphQL */ ` + type Brand { + services: [Service!]! @relationship(type: "HAS_SERVICE", direction: OUT) + name: String! + } + + type Collection { + services: [Service!]! @relationship(type: "HAS_SERVICE", direction: OUT) + } + + type Service { + collection: Collection @relationship(type: "HAS_SERVICE", direction: IN) + } + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs }); + + const query = gql` + query { + brands { + name + services(where: { collectionAggregate: { count: 0 } }) { + collectionAggregate { + count + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Brand) + CALL { + WITH this + MATCH (this)-[this0:HAS_SERVICE]->(this1:Service) + CALL { + WITH this1 + MATCH (this1)<-[this2:HAS_SERVICE]-(this3:Collection) + RETURN count(this3) = $param0 AS var4 + } + WITH * + WHERE var4 = true + CALL { + WITH this1 + MATCH (this1)<-[this5:HAS_SERVICE]-(this6:Collection) + RETURN count(this6) AS var7 + } + WITH this1 { collectionAggregate: { count: var7 } } AS this1 + RETURN collect(this1) AS var8 + } + RETURN this { .name, services: var8 } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 0, + \\"high\\": 0 + } + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/issues/601.test.ts b/packages/graphql/tests/tck/issues/601.test.ts index 00bdfefa4f1..330bf50b354 100644 --- a/packages/graphql/tests/tck/issues/601.test.ts +++ b/packages/graphql/tests/tck/issues/601.test.ts @@ -105,15 +105,20 @@ describe("#601", () => { WITH this1 MATCH (this1)<-[this2:UPLOADED]-(this3:CustomerContact) WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $param4 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) - WITH { fileId: this2.fileId, uploadedAt: apoc.date.convertFormat(toString(this2.uploadedAt), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), node: { __resolveType: \\"CustomerContact\\", __id: id(this3) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this3, relationship: this2 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var4 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this3, edge.relationship AS this2 + RETURN collect({ fileId: this2.fileId, uploadedAt: apoc.date.convertFormat(toString(this2.uploadedAt), \\"iso_zoned_date_time\\", \\"iso_offset_date_time\\"), node: { __resolveType: \\"CustomerContact\\", __id: id(this3) } }) AS var4 + } + RETURN { edges: var4, totalCount: totalCount } AS var5 } - WITH this1 { customerContactConnection: var4 } AS this1 - RETURN collect(this1) AS var5 + WITH this1 { customerContactConnection: var5 } AS this1 + RETURN collect(this1) AS var6 } - RETURN this { documents: var5 } AS this" + RETURN this { documents: var6 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/issues/630.test.ts b/packages/graphql/tests/tck/issues/630.test.ts index c1e9224624c..d5153cab424 100644 --- a/packages/graphql/tests/tck/issues/630.test.ts +++ b/packages/graphql/tests/tck/issues/630.test.ts @@ -81,12 +81,17 @@ describe("Cypher directive", () => { CALL { WITH this0 MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) - WITH { node: { __resolveType: \\"Actor\\", __id: id(this2) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this2, relationship: this1 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { __resolveType: \\"Actor\\", __id: id(this2) } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 } - RETURN collect(this0 { actorsConnection: var3 }) AS this0 + RETURN collect(this0 { actorsConnection: var4 }) AS this0 } RETURN this { movies: this0 } AS this" `); diff --git a/packages/graphql/tests/tck/issues/988.test.ts b/packages/graphql/tests/tck/issues/988.test.ts index 60547daece1..c37206851ee 100644 --- a/packages/graphql/tests/tck/issues/988.test.ts +++ b/packages/graphql/tests/tck/issues/988.test.ts @@ -145,20 +145,30 @@ describe("https://github.com/neo4j/graphql/issues/988", () => { CALL { WITH this MATCH (this)-[this6:MANUFACTURER]->(this7:Manufacturer) - WITH { current: this6.current, node: { name: this7.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this7, relationship: this6 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var8 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this7, edge.relationship AS this6 + RETURN collect({ current: this6.current, node: { name: this7.name } }) AS var8 + } + RETURN { edges: var8, totalCount: totalCount } AS var9 } CALL { WITH this - MATCH (this)-[this9:BRAND]->(this10:Brand) - WITH { current: this9.current, node: { name: this10.name } } AS edge - WITH collect(edge) AS edges + MATCH (this)-[this10:BRAND]->(this11:Brand) + WITH collect({ node: this11, relationship: this10 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var11 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this11, edge.relationship AS this10 + RETURN collect({ current: this10.current, node: { name: this11.name } }) AS var12 + } + RETURN { edges: var12, totalCount: totalCount } AS var13 } - RETURN this { .name, .current, manufacturerConnection: var8, brandConnection: var11 } AS this" + RETURN this { .name, .current, manufacturerConnection: var9, brandConnection: var13 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ diff --git a/packages/graphql/tests/tck/operations/batch/batch-create-auth.test.ts b/packages/graphql/tests/tck/operations/batch/batch-create-auth.test.ts index 7e1cf80aa9d..8324211c3bc 100644 --- a/packages/graphql/tests/tck/operations/batch/batch-create-auth.test.ts +++ b/packages/graphql/tests/tck/operations/batch/batch-create-auth.test.ts @@ -310,7 +310,7 @@ describe("Batch Create, Auth", () => { RETURN c AS this0_website_Website_unique_ignored } WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this0 } CALL { @@ -339,7 +339,7 @@ describe("Batch Create, Auth", () => { RETURN c AS this1_website_Website_unique_ignored } WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this1 } CALL { @@ -358,7 +358,7 @@ describe("Batch Create, Auth", () => { RETURN c AS this2_website_Website_unique_ignored } WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this2 } CALL { @@ -391,7 +391,7 @@ describe("Batch Create, Auth", () => { RETURN c AS this3_website_Website_unique_ignored } WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this3 } CALL { @@ -415,7 +415,7 @@ describe("Batch Create, Auth", () => { RETURN c AS this4_website_Website_unique_ignored } WITH * - WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_0_0_0_after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ($jwt.roles IS NOT NULL AND $authorization_0_after_param2 IN $jwt.roles)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) RETURN this4 } CALL { @@ -519,7 +519,7 @@ describe("Batch Create, Auth", () => { \\"low\\": 2022, \\"high\\": 0 }, - \\"authorization_0_0_0_0_after_param2\\": \\"admin\\", + \\"authorization_0_after_param2\\": \\"admin\\", \\"this1_id\\": \\"2\\", \\"this1_actors0_node_name\\": \\"actor 2\\", \\"this1_actors0_relationship_year\\": { diff --git a/packages/graphql/tests/tck/operations/create.test.ts b/packages/graphql/tests/tck/operations/create.test.ts index bfdd0dcc001..0dedf52fc3f 100644 --- a/packages/graphql/tests/tck/operations/create.test.ts +++ b/packages/graphql/tests/tck/operations/create.test.ts @@ -417,17 +417,22 @@ describe("Cypher Create", () => { WITH create_this1 MATCH (create_this1)<-[create_this2:ACTED_IN]-(create_this3:Actor) WHERE create_this3.name = $create_param0 - WITH { node: { name: create_this3.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this3, relationship: create_this2 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var4 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this3, edge.relationship AS create_this2 + RETURN collect({ node: { name: create_this3.name } }) AS create_var4 + } + RETURN { edges: create_var4, totalCount: totalCount } AS create_var5 } - WITH create_this1 { actorsConnection: create_var4 } AS create_this1 - RETURN collect(create_this1) AS create_var5 + WITH create_this1 { actorsConnection: create_var5 } AS create_this1 + RETURN collect(create_this1) AS create_var6 } - RETURN this0 { .name, movies: create_var5 } AS create_var6 + RETURN this0 { .name, movies: create_var6 } AS create_var7 } - RETURN [create_var6] AS data" + RETURN [create_var7] AS data" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/rfcs/query-limits.test.ts b/packages/graphql/tests/tck/rfcs/query-limits.test.ts index fafad4198ca..a843f11b939 100644 --- a/packages/graphql/tests/tck/rfcs/query-limits.test.ts +++ b/packages/graphql/tests/tck/rfcs/query-limits.test.ts @@ -206,18 +206,17 @@ describe("tck/rfcs/query-limits", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Person) - WITH { node: { id: this1.id } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge + WITH edge.node AS this1, edge.relationship AS this0 + WITH * LIMIT $param1 - RETURN collect(edge) AS var2 + RETURN collect({ node: { id: this1.id } }) AS var2 } - WITH var2 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN { edges: var2, totalCount: totalCount } AS var3 } RETURN this { .id, actorsConnection: var3 } AS this" `); @@ -261,18 +260,17 @@ describe("tck/rfcs/query-limits", () => { CALL { WITH this MATCH (this)<-[this0:ACTED_IN]-(this1:Person) - WITH { node: { id: this1.id } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge + WITH edge.node AS this1, edge.relationship AS this0 + WITH * LIMIT $param1 - RETURN collect(edge) AS var2 + RETURN collect({ node: { id: this1.id } }) AS var2 } - WITH var2 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN { edges: var2, totalCount: totalCount } AS var3 } RETURN this { .id, actorsConnection: var3 } AS this" `); @@ -314,18 +312,17 @@ describe("tck/rfcs/query-limits", () => { CALL { WITH this MATCH (this)<-[this0:PART_OF]-(this1:Show) - WITH { node: { id: this1.id } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount CALL { WITH edges UNWIND edges AS edge - WITH edge + WITH edge.node AS this1, edge.relationship AS this0 + WITH * LIMIT $param0 - RETURN collect(edge) AS var2 + RETURN collect({ node: { id: this1.id } }) AS var2 } - WITH var2 AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS var3 + RETURN { edges: var2, totalCount: totalCount } AS var3 } RETURN this { .name, showsConnection: var3 } AS this" `); diff --git a/packages/graphql/tests/tck/root-connection.test.ts b/packages/graphql/tests/tck/root-connection.test.ts index d81511b9352..814bef532d8 100644 --- a/packages/graphql/tests/tck/root-connection.test.ts +++ b/packages/graphql/tests/tck/root-connection.test.ts @@ -17,10 +17,10 @@ * limitations under the License. */ -import { gql } from "graphql-tag"; import type { DocumentNode } from "graphql"; +import { gql } from "graphql-tag"; import { Neo4jGraphQL } from "../../src"; -import { formatCypher, translateQuery, formatParams } from "./utils/tck-test-utils"; +import { formatCypher, formatParams, translateQuery } from "./utils/tck-test-utils"; describe("Root Connection Query tests", () => { let typeDefs: DocumentNode; @@ -62,15 +62,17 @@ describe("Root Connection Query tests", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title = $param0 - WITH collect(this) AS edges + "MATCH (this0:Movie) + WHERE this0.title = $param0 + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount - WITH { node: this { .title } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + RETURN collect({ node: { title: this0.title } }) AS var1 + } + RETURN { edges: var1, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -96,17 +98,19 @@ describe("Root Connection Query tests", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH collect(this) AS edges + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount - WITH * - ORDER BY this.title ASC - LIMIT $param0 - WITH { node: this { .title } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.title ASC + LIMIT $param0 + RETURN collect({ node: { title: this0.title } }) AS var1 + } + RETURN { edges: var1, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -132,18 +136,20 @@ describe("Root Connection Query tests", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WHERE this.title CONTAINS $param0 - WITH collect(this) AS edges + "MATCH (this0:Movie) + WHERE this0.title CONTAINS $param0 + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount - WITH * - ORDER BY this.title ASC - LIMIT $param1 - WITH { node: this { .title } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.title ASC + LIMIT $param1 + RETURN collect({ node: { title: this0.title } }) AS var1 + } + RETURN { edges: var1, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -177,25 +183,32 @@ describe("Root Connection Query tests", () => { `; const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "MATCH (this:Movie) - WITH collect(this) AS edges + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges WITH edges, size(edges) AS totalCount - UNWIND edges AS this - WITH this, totalCount - WITH * - ORDER BY this.title ASC - LIMIT $param0 CALL { - WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH { node: { name: this1.name } } AS edge - WITH collect(edge) AS edges - WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + WITH * + ORDER BY this0.title ASC + LIMIT $param0 + CALL { + WITH this0 + MATCH (this0)<-[this1:ACTED_IN]-(this2:Actor) + WITH collect({ node: this2, relationship: this1 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this2, edge.relationship AS this1 + RETURN collect({ node: { name: this2.name } }) AS var3 + } + RETURN { edges: var3, totalCount: totalCount } AS var4 + } + RETURN collect({ node: { title: this0.title, actorsConnection: var4 } }) AS var5 } - WITH { node: this { .title, actorsConnection: var2 } } AS edge, totalCount, this - WITH collect(edge) AS edges, totalCount - RETURN { edges: edges, totalCount: totalCount } AS this" + RETURN { edges: var5, totalCount: totalCount } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/sort.test.ts b/packages/graphql/tests/tck/sort.test.ts index f3edbc9b4e4..09be3708b95 100644 --- a/packages/graphql/tests/tck/sort.test.ts +++ b/packages/graphql/tests/tck/sort.test.ts @@ -19,7 +19,7 @@ import { gql } from "graphql-tag"; import { Neo4jGraphQL } from "../../src"; -import { formatCypher, translateQuery, formatParams } from "./utils/tck-test-utils"; +import { formatCypher, formatParams, translateQuery } from "./utils/tck-test-utils"; describe("Cypher sort tests", () => { let typeDefs: string; @@ -27,18 +27,29 @@ describe("Cypher sort tests", () => { beforeAll(() => { typeDefs = ` - type Movie { - id: ID - title: String + interface Production { + id: ID! + title: String! + } + type Movie implements Production { + id: ID! + title: String! + runtime: Int! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") genres: [Genre!]! @relationship(type: "HAS_GENRE", direction: OUT) - totalGenres: Int! + numberOfActors: Int! @cypher( - statement: """ - MATCH (this)-[:HAS_GENRE]->(genre:Genre) - RETURN count(DISTINCT genre) as result - """ - columnName: "result" + statement: "MATCH (actor:Actor)-[:ACTED_IN]->(this) RETURN count(actor) as count" + columnName: "count" ) + totalGenres: Int! + @cypher( + statement: """ + MATCH (this)-[:HAS_GENRE]->(genre:Genre) + RETURN count(DISTINCT genre) as result + """ + columnName: "result" + ) } type Genre { @@ -53,6 +64,29 @@ describe("Cypher sort tests", () => { columnName: "result" ) } + + type Series implements Production { + id: ID! + title: String! + episodes: Int! + } + type Actor { + id: ID! + name: String! + movies: [Movie!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + actedIn: [Production!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + totalScreenTime: Int! + @cypher( + statement: """ + MATCH (this)-[r:ACTED_IN]->(:Movie) + RETURN sum(r.screenTime) as sum + """ + columnName: "sum" + ) + } + interface ActedIn @relationshipProperties { + screenTime: Int! + } `; neoSchema = new Neo4jGraphQL({ @@ -342,7 +376,7 @@ describe("Cypher sort tests", () => { RETURN head(collect(this2)) AS this2 } WITH this1 { .name, totalMovies: this2 } AS this1 - ORDER BY this1.totalMovies ASC + ORDER BY this2 ASC RETURN collect(this1) AS var3 } RETURN this { genres: var3 } AS this" @@ -350,4 +384,187 @@ describe("Cypher sort tests", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); }); + + test("Connection top level", async () => { + const query = gql` + query { + moviesConnection(first: 2, sort: { title: DESC, numberOfActors: ASC }) { + totalCount + edges { + node { + title + actorsConnection { + edges { + node { + name + totalScreenTime + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this0:Movie) + WITH collect({ node: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this0 + CALL { + WITH this0 + CALL { + WITH this0 + WITH this0 AS this + MATCH (actor:Actor)-[:ACTED_IN]->(this) RETURN count(actor) as count + } + UNWIND count AS this1 + RETURN head(collect(this1)) AS this1 + } + WITH * + ORDER BY this0.title DESC, this1 ASC + LIMIT $param0 + CALL { + WITH this0 + MATCH (this0)<-[this2:ACTED_IN]-(this3:Actor) + WITH collect({ node: this3, relationship: this2 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this3, edge.relationship AS this2 + CALL { + WITH this3 + CALL { + WITH this3 + WITH this3 AS this + MATCH (this)-[r:ACTED_IN]->(:Movie) + RETURN sum(r.screenTime) as sum + } + UNWIND sum AS this4 + RETURN head(collect(this4)) AS this4 + } + RETURN collect({ node: { name: this3.name, totalScreenTime: this4 } }) AS var5 + } + RETURN { edges: var5, totalCount: totalCount } AS var6 + } + RETURN collect({ node: { title: this0.title, actorsConnection: var6 } }) AS var7 + } + RETURN { edges: var7, totalCount: totalCount } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 2, + \\"high\\": 0 + } + }" + `); + }); + + test("Connection nested", async () => { + const query = gql` + query { + actors { + moviesConnection(first: 2, sort: { node: { title: DESC, numberOfActors: ASC } }) { + totalCount + edges { + node { + title + actorsConnection { + edges { + node { + name + totalScreenTime + } + } + } + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + `; + + const result = await translateQuery(neoSchema, query); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Actor) + CALL { + WITH this + MATCH (this)-[this0:ACTED_IN]->(this1:Movie) + WITH collect({ node: this1, relationship: this0 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + CALL { + WITH this1 + CALL { + WITH this1 + WITH this1 AS this + MATCH (actor:Actor)-[:ACTED_IN]->(this) RETURN count(actor) as count + } + UNWIND count AS this2 + RETURN head(collect(this2)) AS this2 + } + WITH * + ORDER BY this1.title DESC, this2 ASC + LIMIT $param0 + CALL { + WITH this1 + MATCH (this1)<-[this3:ACTED_IN]-(this4:Actor) + WITH collect({ node: this4, relationship: this3 }) AS edges + WITH edges, size(edges) AS totalCount + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this4, edge.relationship AS this3 + CALL { + WITH this4 + CALL { + WITH this4 + WITH this4 AS this + MATCH (this)-[r:ACTED_IN]->(:Movie) + RETURN sum(r.screenTime) as sum + } + UNWIND sum AS this5 + RETURN head(collect(this5)) AS this5 + } + RETURN collect({ node: { name: this4.name, totalScreenTime: this5 } }) AS var6 + } + RETURN { edges: var6, totalCount: totalCount } AS var7 + } + RETURN collect({ node: { title: this1.title, actorsConnection: var7 } }) AS var8 + } + RETURN { edges: var8, totalCount: totalCount } AS var9 + } + RETURN this { moviesConnection: var9 } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": { + \\"low\\": 2, + \\"high\\": 0 + } + }" + `); + }); }); diff --git a/packages/graphql/tests/tck/subscriptions/create.test.ts b/packages/graphql/tests/tck/subscriptions/create.test.ts index 482f9f7bd24..ebfbcd6666d 100644 --- a/packages/graphql/tests/tck/subscriptions/create.test.ts +++ b/packages/graphql/tests/tck/subscriptions/create.test.ts @@ -122,14 +122,19 @@ describe("Subscriptions metadata on create", () => { CALL { WITH this0 MATCH (this0)<-[create_this0:ACTED_IN]-(create_this1:Actor) - WITH { screenTime: create_this0.screenTime, node: { name: create_this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this1, relationship: create_this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this1, edge.relationship AS create_this0 + RETURN collect({ screenTime: create_this0.screenTime, node: { name: create_this1.name } }) AS create_var2 + } + RETURN { edges: create_var2, totalCount: totalCount } AS create_var3 } - RETURN this0 { .title, actorsConnection: create_var2 } AS create_var3 + RETURN this0 { .title, actorsConnection: create_var3 } AS create_var4 } - RETURN [create_var3] AS data, meta" + RETURN [create_var4] AS data, meta" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -205,14 +210,19 @@ describe("Subscriptions metadata on create", () => { CALL { WITH this0 MATCH (this0)<-[create_this0:ACTED_IN]-(create_this1:Actor) - WITH { node: { name: create_this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this1, relationship: create_this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this1, edge.relationship AS create_this0 + RETURN collect({ node: { name: create_this1.name } }) AS create_var2 + } + RETURN { edges: create_var2, totalCount: totalCount } AS create_var3 } - RETURN this0 { .title, actorsConnection: create_var2 } AS create_var3 + RETURN this0 { .title, actorsConnection: create_var3 } AS create_var4 } - RETURN [create_var3] AS data, meta" + RETURN [create_var4] AS data, meta" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ @@ -313,14 +323,19 @@ describe("Subscriptions metadata on create", () => { CALL { WITH this0 MATCH (this0)<-[create_this0:ACTED_IN]-(create_this1:Actor) - WITH { screenTime: create_this0.screenTime, node: { name: create_this1.name } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: create_this1, relationship: create_this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS create_var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS create_this1, edge.relationship AS create_this0 + RETURN collect({ screenTime: create_this0.screenTime, node: { name: create_this1.name } }) AS create_var2 + } + RETURN { edges: create_var2, totalCount: totalCount } AS create_var3 } - RETURN this0 { .title, actorsConnection: create_var2 } AS create_var3 + RETURN this0 { .title, actorsConnection: create_var3 } AS create_var4 } - RETURN [create_var3] AS data, meta" + RETURN [create_var4] AS data, meta" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ diff --git a/packages/graphql/tests/tck/undirected-relationships/query-direction-connection.test.ts b/packages/graphql/tests/tck/undirected-relationships/query-direction-connection.test.ts index f2053dab07f..52de48424f5 100644 --- a/packages/graphql/tests/tck/undirected-relationships/query-direction-connection.test.ts +++ b/packages/graphql/tests/tck/undirected-relationships/query-direction-connection.test.ts @@ -55,12 +55,17 @@ describe("QueryDirection in relationships connection", () => { CALL { WITH this MATCH (this)-[this0:FRIENDS_WITH]-(this1:User) - WITH { node: { __resolveType: \\"User\\", __id: id(this1) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { __resolveType: \\"User\\", __id: id(this1) } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { friendsConnection: var2 } AS this" + RETURN this { friendsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -94,12 +99,17 @@ describe("QueryDirection in relationships connection", () => { CALL { WITH this MATCH (this)-[this0:FRIENDS_WITH]->(this1:User) - WITH { node: { __resolveType: \\"User\\", __id: id(this1) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { __resolveType: \\"User\\", __id: id(this1) } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { friendsConnection: var2 } AS this" + RETURN this { friendsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -132,12 +142,17 @@ describe("QueryDirection in relationships connection", () => { CALL { WITH this MATCH (this)-[this0:FRIENDS_WITH]-(this1:User) - WITH { node: { __resolveType: \\"User\\", __id: id(this1) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { __resolveType: \\"User\\", __id: id(this1) } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { friendsConnection: var2 } AS this" + RETURN this { friendsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); diff --git a/packages/graphql/tests/tck/undirected-relationships/undirected-connection.test.ts b/packages/graphql/tests/tck/undirected-relationships/undirected-connection.test.ts index 91c85469165..4879717fe29 100644 --- a/packages/graphql/tests/tck/undirected-relationships/undirected-connection.test.ts +++ b/packages/graphql/tests/tck/undirected-relationships/undirected-connection.test.ts @@ -54,12 +54,17 @@ describe("Undirected connections", () => { CALL { WITH this MATCH (this)-[this0:FRIENDS_WITH]-(this1:User) - WITH { node: { __resolveType: \\"User\\", __id: id(this1) } } AS edge - WITH collect(edge) AS edges + WITH collect({ node: this1, relationship: this0 }) AS edges WITH edges, size(edges) AS totalCount - RETURN { edges: edges, totalCount: totalCount } AS var2 + CALL { + WITH edges + UNWIND edges AS edge + WITH edge.node AS this1, edge.relationship AS this0 + RETURN collect({ node: { __resolveType: \\"User\\", __id: id(this1) } }) AS var2 + } + RETURN { edges: var2, totalCount: totalCount } AS var3 } - RETURN this { friendsConnection: var2 } AS this" + RETURN this { friendsConnection: var3 } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); diff --git a/packages/graphql/tests/utils/builders/context-builder.ts b/packages/graphql/tests/utils/builders/context-builder.ts index d4ebd62db5f..3baad14ee0e 100644 --- a/packages/graphql/tests/utils/builders/context-builder.ts +++ b/packages/graphql/tests/utils/builders/context-builder.ts @@ -50,6 +50,7 @@ export class ContextBuilder extends Builder { test("should remove all directives", () => { const initial = ` - type User @auth @exclude { - id: ID @auth @private @readonly @writeonly - name: String @auth @private @readonly @writeonly - email: String @auth @private @readonly @writeonly - password: String @auth @private @readonly @writeonly + type User @authentication @authorization { + id: ID @id @private @unique + name: String @authentication @authorization @private + email: String @cypher @private + password: String @private cars: [Car!]! @relationship(type: "HAS_CAR", direction: OUT, aggregate: false) - bikes: [Car!]! @relationship(type: "HAS_CAR", direction: OUT) + bikes: [Car!]! @relationship(type: "HAS_CAR", direction: OUT) } type Car @query(read: false, aggregate: false) @mutation(operations: []), @subscription(events: []) { @@ -50,9 +50,9 @@ describe("filterDocument", () => { expect(print(filtered)).toMatchInlineSnapshot(` "type User { - id: ID + id: ID @id @unique name: String - email: String + email: String @cypher password: String cars: [Car!]! @relationship(type: \\"HAS_CAR\\", direction: OUT, aggregate: true) bikes: [Car!]! @relationship(type: \\"HAS_CAR\\", direction: OUT, aggregate: true) diff --git a/packages/ogm/src/utils/filter-document.ts b/packages/ogm/src/utils/filter-document.ts index e09ac2cab46..5da26ac76dd 100644 --- a/packages/ogm/src/utils/filter-document.ts +++ b/packages/ogm/src/utils/filter-document.ts @@ -22,14 +22,10 @@ import type { Neo4jGraphQLConstructor } from "@neo4j/graphql"; import { mergeTypeDefs } from "@graphql-tools/merge"; const excludedDirectives = [ - "auth", "authentication", "authorization", "subscriptionsAuthorization", - "exclude", "private", - "readonly", - "writeonly", "query", "mutation", "subscription", @@ -41,8 +37,8 @@ const excludedDirectives = [ export function filterDocument(typeDefs: Neo4jGraphQLConstructor["typeDefs"]): DocumentNode { // hack to keep aggregation enabled for OGM const schemaExtension = ` - extend schema @query(read: true, aggregate: true) - @mutation(operations: [CREATE, UPDATE, DELETE]) + extend schema @query(read: true, aggregate: true) + @mutation(operations: [CREATE, UPDATE, DELETE]) @subscription(events: [CREATED, UPDATED, DELETED, RELATIONSHIP_CREATED, RELATIONSHIP_DELETED])`; const merged = mergeTypeDefs( Array.isArray(typeDefs) ? (typeDefs as string[]).concat(schemaExtension) : [typeDefs as string, schemaExtension] diff --git a/packages/ogm/tests/integration/ogm.int.test.ts b/packages/ogm/tests/integration/ogm.int.test.ts index 391167307b5..b36004ef7b7 100644 --- a/packages/ogm/tests/integration/ogm.int.test.ts +++ b/packages/ogm/tests/integration/ogm.int.test.ts @@ -78,11 +78,11 @@ describe("OGM", () => { await session.close(); }); - test("should filter the document and remove directives such as auth", async () => { + test("should filter the document and remove directives such as authentication", async () => { const session = driver.session(); const typeDefs = ` - type ${typeMovie} @auth(rules: [{ isAuthenticated: true }]){ + type ${typeMovie} @authentication { id: ID } `; diff --git a/packages/package-tests/babel/package.json b/packages/package-tests/babel/package.json index 1817a88c2f9..6f55bae3bdf 100644 --- a/packages/package-tests/babel/package.json +++ b/packages/package-tests/babel/package.json @@ -13,8 +13,8 @@ "neo4j-driver": "^5.8.0" }, "devDependencies": { - "@babel/core": "7.23.5", + "@babel/core": "7.23.7", "@babel/node": "7.22.19", - "@babel/preset-env": "7.23.5" + "@babel/preset-env": "7.23.8" } } diff --git a/yarn.lock b/yarn.lock index 535c5aeec04..19d9790f2cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -99,15 +99,15 @@ __metadata: languageName: node linkType: hard -"@apollo/composition@npm:2.6.1": - version: 2.6.1 - resolution: "@apollo/composition@npm:2.6.1" +"@apollo/composition@npm:2.6.2": + version: 2.6.2 + resolution: "@apollo/composition@npm:2.6.2" dependencies: - "@apollo/federation-internals": 2.6.1 - "@apollo/query-graphs": 2.6.1 + "@apollo/federation-internals": 2.6.2 + "@apollo/query-graphs": 2.6.2 peerDependencies: graphql: ^16.5.0 - checksum: 3332842e7b320457bedc7d6e99ee76ca16595e8d531cdec33a3deaf53a06932900edea839ec92d9a8b314caff8a5b4021efa21e14c98a291a5da84ee6b5530ca + checksum: c62e772e75db6ab0b282c7c2d1d249a6e08e3fb5472f9cc750775dcfdc03467cd8fb44919f701d9e4f5d6b49eb5e6b6984b0d32fbbba767a9f1b0895e2271095 languageName: node linkType: hard @@ -125,9 +125,9 @@ __metadata: languageName: node linkType: hard -"@apollo/federation-internals@npm:2.6.1": - version: 2.6.1 - resolution: "@apollo/federation-internals@npm:2.6.1" +"@apollo/federation-internals@npm:2.6.2": + version: 2.6.2 + resolution: "@apollo/federation-internals@npm:2.6.2" dependencies: "@types/uuid": ^9.0.0 chalk: ^4.1.0 @@ -135,7 +135,7 @@ __metadata: uuid: ^9.0.0 peerDependencies: graphql: ^16.5.0 - checksum: af7293ef180ba2ec5f6300d37ac4240f3cdd8aa966a7cae0e175e52835274f6c89b110b3ab55b9494b380ea02261e0e997be806cea4e4c7ec3a2a0f7b591f900 + checksum: 0b246190a6a9000881b718e88efafee6039ad2fe7858b83ec509dc6a9a83c44f1eff666308d14eb2597f73ca38e8453c6efb75e2240b83f960dc12a0f019153d languageName: node linkType: hard @@ -169,13 +169,13 @@ __metadata: languageName: node linkType: hard -"@apollo/gateway@npm:2.6.1": - version: 2.6.1 - resolution: "@apollo/gateway@npm:2.6.1" +"@apollo/gateway@npm:2.6.2": + version: 2.6.2 + resolution: "@apollo/gateway@npm:2.6.2" dependencies: - "@apollo/composition": 2.6.1 - "@apollo/federation-internals": 2.6.1 - "@apollo/query-planner": 2.6.1 + "@apollo/composition": 2.6.2 + "@apollo/federation-internals": 2.6.2 + "@apollo/query-planner": 2.6.2 "@apollo/server-gateway-interface": ^1.1.0 "@apollo/usage-reporting-protobuf": ^4.1.0 "@apollo/utils.createhash": ^2.0.0 @@ -193,7 +193,7 @@ __metadata: node-fetch: ^2.6.7 peerDependencies: graphql: ^16.5.0 - checksum: 01bfc84b7344db334efcc98fc18b9c0ea16389b8051449177c4fab1b746e76b9a7d67ddbf9fc08c48f42732c0579ca4a731272aa68b9bdc0ad839b2d74c1f3d3 + checksum: 48f1deaeb1f98cfe96e0d2139ad38670fe6de457b87a788aa6bd314f5ea9f0e6ab025997858e9e958ec4999758c2929a371befd8261b89c892a2e34ac7e04910 languageName: node linkType: hard @@ -220,33 +220,33 @@ __metadata: languageName: node linkType: hard -"@apollo/query-graphs@npm:2.6.1": - version: 2.6.1 - resolution: "@apollo/query-graphs@npm:2.6.1" +"@apollo/query-graphs@npm:2.6.2": + version: 2.6.2 + resolution: "@apollo/query-graphs@npm:2.6.2" dependencies: - "@apollo/federation-internals": 2.6.1 + "@apollo/federation-internals": 2.6.2 deep-equal: ^2.0.5 ts-graphviz: ^1.5.4 uuid: ^9.0.0 peerDependencies: graphql: ^16.5.0 - checksum: 932acd852c10e2dad8e67902e302602ecaeee5fd1bc98fe4c58bec45fadb20b7a5ba7384f33374e54c08c3e935254b3c88275b6bec9186c378fb89aa578a4853 + checksum: 57bec3b75a535ea832e67c640e773a0fa34e500fe6f8633e14b677cc188f687fff67490af6800679c5d240e57de9e2e682c4ac6c260bd81ba579b4836657b58c languageName: node linkType: hard -"@apollo/query-planner@npm:2.6.1": - version: 2.6.1 - resolution: "@apollo/query-planner@npm:2.6.1" +"@apollo/query-planner@npm:2.6.2": + version: 2.6.2 + resolution: "@apollo/query-planner@npm:2.6.2" dependencies: - "@apollo/federation-internals": 2.6.1 - "@apollo/query-graphs": 2.6.1 + "@apollo/federation-internals": 2.6.2 + "@apollo/query-graphs": 2.6.2 "@apollo/utils.keyvaluecache": ^2.1.0 chalk: ^4.1.0 deep-equal: ^2.0.5 pretty-format: ^29.0.0 peerDependencies: graphql: ^16.5.0 - checksum: 14c8cf1447241d6b2273be8de12c54e45cb963d49807ac9af4ac906e2edb70e8c962ae283c766700333cef56d14dcfaca8741d60ea2070d994fb1a8f50023f43 + checksum: 342220639874041841558637b0aeb85e7d75c41fdef993b4176950a6498ffa26ffc8220cf8998e1280bda12b1da59231d8dc438979bfda247fee5bd7a71d77a7 languageName: node linkType: hard @@ -278,9 +278,9 @@ __metadata: languageName: node linkType: hard -"@apollo/server@npm:4.9.5": - version: 4.9.5 - resolution: "@apollo/server@npm:4.9.5" +"@apollo/server@npm:4.10.0": + version: 4.10.0 + resolution: "@apollo/server@npm:4.10.0" dependencies: "@apollo/cache-control-types": ^1.0.3 "@apollo/server-gateway-interface": ^1.1.1 @@ -298,7 +298,6 @@ __metadata: "@types/express-serve-static-core": ^4.17.30 "@types/node-fetch": ^2.6.1 async-retry: ^1.2.1 - body-parser: ^1.20.0 cors: ^2.8.5 express: ^4.17.1 loglevel: ^1.6.8 @@ -310,7 +309,7 @@ __metadata: whatwg-mimetype: ^3.0.0 peerDependencies: graphql: ^16.6.0 - checksum: 52aac2ef0665a776b2da8930b2a6e31b652a9a3c5b2e48e56d323e40b618acae091c69dc04ebed5355a36e22b793ceb31baa04afe516205c67c2db87c0fb01a0 + checksum: 902df9ea634ef52ba96b26e923d86b5093f678bcd9e76e4c4f57c26f5450064603449e095b8c8d2a21316c8a9277444e7d9145af8811ddc481d1992bf4123c39 languageName: node linkType: hard @@ -1736,15 +1735,15 @@ __metadata: languageName: node linkType: hard -"@codemirror/commands@npm:6.3.2": - version: 6.3.2 - resolution: "@codemirror/commands@npm:6.3.2" +"@codemirror/commands@npm:6.3.3": + version: 6.3.3 + resolution: "@codemirror/commands@npm:6.3.3" dependencies: "@codemirror/language": ^6.0.0 - "@codemirror/state": ^6.2.0 + "@codemirror/state": ^6.4.0 "@codemirror/view": ^6.0.0 "@lezer/common": ^1.1.0 - checksum: 683c444d8e6ad889ab5efd0d742b0fa28b78c8cad63276ec60d298b13d4939c8bd7e1d6fd3535645b8d255147de0d3aef46d89a29c19d0af58a7f2914bdcb3ab + checksum: 7d23aecc973823969434b839aefa9a98bb47212d2ce0e6869ae903adbb5233aad22a760788fb7bb6eb45b00b01a4932fb93ad43bacdcbc0215e7500cf54b17bb languageName: node linkType: hard @@ -1775,17 +1774,17 @@ __metadata: languageName: node linkType: hard -"@codemirror/language@npm:6.9.3": - version: 6.9.3 - resolution: "@codemirror/language@npm:6.9.3" +"@codemirror/language@npm:6.10.0": + version: 6.10.0 + resolution: "@codemirror/language@npm:6.10.0" dependencies: "@codemirror/state": ^6.0.0 - "@codemirror/view": ^6.0.0 + "@codemirror/view": ^6.23.0 "@lezer/common": ^1.1.0 "@lezer/highlight": ^1.0.0 "@lezer/lr": ^1.0.0 style-mod: ^4.0.0 - checksum: 774a40bc91c748d418a9a774161a5b083061124e4439bb753072bc657ec4c4784f595161c10c7c3935154b22291bf6dc74c9abe827033db32e217ac3963478f3 + checksum: 3bfd9968f5a34ce22434489a5b226db5f3bc454a1ae7c4381587ff4270ac6af61b10f93df560cb73e9a77cc13d4843722a7a7b94dbed02a3ab1971dd329b9e81 languageName: node linkType: hard @@ -1825,21 +1824,21 @@ __metadata: languageName: node linkType: hard -"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.1.4, @codemirror/state@npm:^6.2.0": - version: 6.2.1 - resolution: "@codemirror/state@npm:6.2.1" - checksum: d12a321d0471b264b9d3259042bff913a8b939e8d28d408ff452004538a71ca9d5329df3f8a1d8a9183f5b42a7ef5b200737bcab1065714f5ae8e0a5ba9d59d3 +"@codemirror/state@npm:^6.0.0, @codemirror/state@npm:^6.2.0, @codemirror/state@npm:^6.4.0": + version: 6.4.0 + resolution: "@codemirror/state@npm:6.4.0" + checksum: c5236fe5786f1b85d17273a5c17fa8aeb063658c1404ab18caeb6e7591663ec96b65d453ab8162f75839c3801b04cd55ba4bc48f44cb61ebfeeee383f89553c7 languageName: node linkType: hard -"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.9.0": - version: 6.20.2 - resolution: "@codemirror/view@npm:6.20.2" +"@codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.9.0": + version: 6.23.0 + resolution: "@codemirror/view@npm:6.23.0" dependencies: - "@codemirror/state": ^6.1.4 + "@codemirror/state": ^6.4.0 style-mod: ^4.1.0 w3c-keyname: ^2.2.4 - checksum: eaf47726bb94b40f12c6f1d3494b558addaa7cd98a4ff05bfea7439c03cca3967d73380346ea8b06021478c21ec336a74665c9acb44e2b0280d5e0da4714387b + checksum: 6e5f2314a3da2c724dc6a525654d949d3f2fcf7009f4d85f980d52ddc885c8969717e903ca1d9132afbe7c524af5d19bff8285fd394106282a965ae83aa47db4 languageName: node linkType: hard @@ -2129,10 +2128,10 @@ __metadata: languageName: node linkType: hard -"@eslint/js@npm:8.55.0": - version: 8.55.0 - resolution: "@eslint/js@npm:8.55.0" - checksum: fa33ef619f0646ed15649b0c2e313e4d9ccee8425884bdbfc78020d6b6b64c0c42fa9d83061d0e6158e1d4274f03f0f9008786540e2efab8fcdc48082259908c +"@eslint/js@npm:8.56.0": + version: 8.56.0 + resolution: "@eslint/js@npm:8.56.0" + checksum: 5804130574ef810207bdf321c265437814e7a26f4e6fac9b496de3206afd52f533e09ec002a3be06cd9adcc9da63e727f1883938e663c4e4751c007d5b58e539 languageName: node linkType: hard @@ -3203,6 +3202,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.20": + version: 0.3.20 + resolution: "@jridgewell/trace-mapping@npm:0.3.20" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: cd1a7353135f385909468ff0cf20bdd37e59f2ee49a13a966dedf921943e222082c583ade2b579ff6cd0d8faafcb5461f253e1bf2a9f48fec439211fdbe788f5 + languageName: node + linkType: hard + "@leichtgewicht/ip-codec@npm:^2.0.1": version: 2.0.4 resolution: "@leichtgewicht/ip-codec@npm:2.0.4" @@ -3518,21 +3527,21 @@ __metadata: languageName: node linkType: hard -"@neo4j-ndl/base@npm:2.0.7, @neo4j-ndl/base@npm:^2.0.7": - version: 2.0.7 - resolution: "@neo4j-ndl/base@npm:2.0.7" - checksum: 1f5792dc0ded8e4d72bcfdcb7d11a79c7ddf31d48c4ad1ae117ba14976d7b6bf89ccd159183163316919d46f74bc2b56389d326d6af635d4089301f295ef0e03 +"@neo4j-ndl/base@npm:2.3.0, @neo4j-ndl/base@npm:^2.3.0": + version: 2.3.0 + resolution: "@neo4j-ndl/base@npm:2.3.0" + checksum: c8d7da4516a3c2216db3a44471ed16b4d46cf4c2641eadddcc04faf24f11156a602f0fb4b13379ccd6f4d74ce7df0e7e080591c8746334d5be20ed515106d7a5 languageName: node linkType: hard -"@neo4j-ndl/react@npm:2.0.14": - version: 2.0.14 - resolution: "@neo4j-ndl/react@npm:2.0.14" +"@neo4j-ndl/react@npm:2.3.1": + version: 2.3.1 + resolution: "@neo4j-ndl/react@npm:2.3.1" dependencies: "@floating-ui/react": 0.25.1 "@heroicons/react": 2.0.13 "@neo4j-cypher/react-codemirror": ^1.0.1 - "@neo4j-ndl/base": ^2.0.7 + "@neo4j-ndl/base": ^2.3.0 "@table-nav/core": 0.0.7 "@table-nav/react": 0.0.7 "@tanstack/react-table": ^8.9.3 @@ -3549,10 +3558,11 @@ __metadata: react-table: ^7.7.0 react-use: ^17.4.0 tinycolor2: ^1.4.2 + usehooks-ts: 2.9.1 peerDependencies: "@heroicons/react": 2.0.13 react: ">=16.8.0" - checksum: be5ab15da9f8fe4b57310caf01972e15de83dc42f60abb9f731410385429db116cd388d1bb934cd6bac64995c168a0f12251ca81f38b080f34e34e18c80fea63 + checksum: 69d160bf5fb4af372d580c35faaffffb7b3638a5e0f4195257b7d876c56a70c6fb8b4876b3b175af65ba6adf933105877596357f32b1b144d00bb21e34a3efd2 languageName: node linkType: hard @@ -3567,27 +3577,27 @@ __metadata: version: 0.0.0-use.local resolution: "@neo4j/graphql-amqp-subscriptions-engine@workspace:packages/graphql-amqp-subscriptions-engine" dependencies: - "@apollo/server": 4.9.5 + "@apollo/server": 4.10.0 "@neo4j/graphql": ^4.0.0 "@types/amqplib": 0.10.4 "@types/body-parser": 1.19.5 "@types/cors": 2.8.17 "@types/debug": 4.1.12 - "@types/jest": 29.5.10 - "@types/node": 20.10.3 + "@types/jest": 29.5.11 + "@types/node": 20.10.8 amqplib: 0.10.3 body-parser: ^1.20.2 camelcase: 6.3.0 cors: ^2.8.5 - graphql-ws: 5.14.2 + graphql-ws: 5.14.3 jest: 29.7.0 - neo4j-driver: 5.15.0 + neo4j-driver: 5.16.0 pluralize: 8.0.0 randomstring: 1.3.0 supertest: 6.3.3 ts-jest: 29.1.1 typescript: 5.1.6 - ws: 8.14.2 + ws: 8.16.0 peerDependencies: "@neo4j/graphql": ^4.0.0-beta.0 languageName: unknown @@ -3601,14 +3611,14 @@ __metadata: "@graphql-codegen/plugin-helpers": ^5.0.0 "@graphql-codegen/typescript": ^4.0.0 "@graphql-tools/merge": ^9.0.0 - "@neo4j/graphql": ^4.4.4 - "@types/jest": 29.5.10 - "@types/node": 20.10.3 + "@neo4j/graphql": ^4.4.5 + "@types/jest": 29.5.11 + "@types/node": 20.10.8 camelcase: 6.3.0 graphql-tag: 2.12.6 jest: 29.7.0 jsonwebtoken: 9.0.2 - libnpmsearch: 7.0.0 + libnpmsearch: 7.0.1 npm-run-all: 4.1.5 pluralize: 8.0.0 prettier: ^2.7.1 @@ -3627,33 +3637,32 @@ __metadata: resolution: "@neo4j/graphql-toolbox@workspace:packages/graphql-toolbox" dependencies: "@codemirror/autocomplete": 6.11.1 - "@codemirror/commands": 6.3.2 + "@codemirror/commands": 6.3.3 "@codemirror/lang-javascript": 6.2.1 - "@codemirror/language": 6.9.3 + "@codemirror/language": 6.10.0 "@dnd-kit/core": 6.1.0 "@dnd-kit/modifiers": 7.0.0 "@dnd-kit/sortable": 8.0.0 "@graphiql/react": 0.20.2 - "@neo4j-ndl/base": 2.0.7 - "@neo4j-ndl/react": 2.0.14 - "@neo4j/graphql": 4.4.4 + "@neo4j-ndl/base": 2.3.0 + "@neo4j-ndl/react": 2.3.1 + "@neo4j/graphql": 4.4.5 "@neo4j/introspector": 2.0.0 "@playwright/test": 1.40.1 "@tsconfig/create-react-app": 2.0.1 "@types/codemirror": 5.60.15 "@types/lodash.debounce": 4.0.9 - "@types/markdown-it": 13.0.7 "@types/prettier": 2.7.3 - "@types/react-dom": 18.2.17 + "@types/react-dom": 18.2.18 "@types/webpack": 5.28.5 autoprefixer: 10.4.16 - classnames: 2.3.2 + classnames: 2.5.1 cm6-graphql: 0.0.12 codemirror: 6.0.1 compression-webpack-plugin: 10.0.0 copy-webpack-plugin: 11.0.0 cross-env: 7.0.3 - css-loader: 6.8.1 + css-loader: 6.9.0 dotenv: 16.3.1 dotenv-webpack: 8.0.1 fork-ts-checker-webpack-plugin: 9.0.2 @@ -3662,27 +3671,26 @@ __metadata: graphql-query-complexity: 0.12.0 html-inline-script-webpack-plugin: 3.2.1 html-webpack-inline-source-plugin: 0.0.10 - html-webpack-plugin: 5.5.3 + html-webpack-plugin: 5.6.0 jest: 29.7.0 jest-environment-jsdom: 29.7.0 - markdown-it: 13.0.2 - neo4j-driver: 5.15.0 + neo4j-driver: 5.16.0 node-polyfill-webpack-plugin: 2.0.1 parse5: 7.1.2 - postcss: 8.4.32 - postcss-loader: 7.3.3 + postcss: 8.4.33 + postcss-loader: 7.3.4 prettier: 3.0.0 process: 0.11.10 randomstring: 1.3.0 react: 18.2.0 react-dom: 18.2.0 - style-loader: 3.3.3 - tailwindcss: 3.3.5 - terser-webpack-plugin: 5.3.9 + style-loader: 3.3.4 + tailwindcss: 3.4.1 + terser-webpack-plugin: 5.3.10 thememirror: 2.0.1 ts-jest: 29.1.1 ts-loader: 9.5.1 - ts-node: 10.9.1 + ts-node: 10.9.2 tsconfig-paths-webpack-plugin: 4.1.0 typescript: 5.1.6 webpack: 5.89.0 @@ -3693,12 +3701,12 @@ __metadata: languageName: unknown linkType: soft -"@neo4j/graphql@4.4.4, @neo4j/graphql@^4.0.0, @neo4j/graphql@^4.0.0-beta.0, @neo4j/graphql@^4.4.4, @neo4j/graphql@workspace:packages/graphql": +"@neo4j/graphql@4.4.5, @neo4j/graphql@^4.0.0, @neo4j/graphql@^4.0.0-beta.0, @neo4j/graphql@^4.4.5, @neo4j/graphql@workspace:packages/graphql": version: 0.0.0-use.local resolution: "@neo4j/graphql@workspace:packages/graphql" dependencies: - "@apollo/gateway": 2.6.1 - "@apollo/server": 4.9.5 + "@apollo/gateway": 2.6.2 + "@apollo/server": 4.10.0 "@apollo/subgraph": ^2.2.3 "@faker-js/faker": 8.3.1 "@graphql-tools/merge": ^9.0.0 @@ -3708,9 +3716,9 @@ __metadata: "@neo4j/cypher-builder": ^1.7.1 "@types/deep-equal": 1.0.4 "@types/is-uuid": 1.0.2 - "@types/jest": 29.5.10 + "@types/jest": 29.5.11 "@types/jsonwebtoken": 9.0.5 - "@types/node": 20.10.3 + "@types/node": 20.10.8 "@types/pluralize": 0.0.33 "@types/randomstring": 1.1.11 "@types/semver": 7.5.6 @@ -3726,16 +3734,16 @@ __metadata: graphql-parse-resolve-info: ^4.12.3 graphql-relay: ^0.10.0 graphql-tag: 2.12.6 - graphql-ws: 5.14.2 + graphql-ws: 5.14.3 is-uuid: 1.0.2 jest: 29.7.0 jest-extended: 4.0.2 jose: ^5.0.0 jwks-rsa: 3.1.0 - koa: 2.14.2 + koa: 2.15.0 koa-jwt: 4.0.4 koa-router: 12.0.1 - libnpmsearch: 7.0.0 + libnpmsearch: 7.0.1 mock-jwks: 1.0.10 nock: 13.4.0 npm-run-all: 4.1.5 @@ -3745,11 +3753,10 @@ __metadata: semver: ^7.5.4 supertest: 6.3.3 ts-jest: 29.1.1 - ts-node: 10.9.1 + ts-node: 10.9.2 typescript: 5.1.6 typescript-memoize: ^1.1.1 - uuid: ^9.0.0 - ws: 8.14.2 + ws: 8.16.0 peerDependencies: graphql: ^16.0.0 neo4j-driver: ^5.8.0 @@ -3767,8 +3774,8 @@ __metadata: resolution: "@neo4j/introspector@workspace:packages/introspector" dependencies: "@neo4j/graphql": ^4.0.0 - "@types/jest": 29.5.10 - "@types/node": 20.10.3 + "@types/jest": 29.5.11 + "@types/node": 20.10.8 "@types/pluralize": 0.0.33 camelcase: ^6.3.0 debug: ^4.3.4 @@ -3882,112 +3889,112 @@ __metadata: languageName: node linkType: hard -"@parcel/bundler-default@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/bundler-default@npm:2.10.3" +"@parcel/bundler-default@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/bundler-default@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/graph": 3.0.3 - "@parcel/plugin": 2.10.3 - "@parcel/rust": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/graph": 3.1.0 + "@parcel/plugin": 2.11.0 + "@parcel/rust": 2.11.0 + "@parcel/utils": 2.11.0 nullthrows: ^1.1.1 - checksum: 2fd80acec4aeb4306b6ad29d381e8c77003576efc9c057a24fac80c363b3a8ba0c4b7bbea1587312fe3f88ab1fbcac085dbb90cfee5759d33fa085bc550d7191 + checksum: 6541cd9b82cd403f25fa3c1ee50d7cf9d27ed8f51256b1e4ead0655335128f9225c87e3118203b1b48e29f157a69dab5797065abb78fb03a409ad2765ea3a1e5 languageName: node linkType: hard -"@parcel/cache@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/cache@npm:2.10.3" +"@parcel/cache@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/cache@npm:2.11.0" dependencies: - "@parcel/fs": 2.10.3 - "@parcel/logger": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/fs": 2.11.0 + "@parcel/logger": 2.11.0 + "@parcel/utils": 2.11.0 lmdb: 2.8.5 peerDependencies: - "@parcel/core": ^2.10.3 - checksum: b7e9a000a78786c1adda97d23f29d5c35695637db5de305d7fb6d4f8e88bed9c98065f4d0b094a131b2deb392451b897635fd2234fa4cf3a8a6667ae0b6af5db + "@parcel/core": ^2.11.0 + checksum: bcb6ce02244e8bbc3c23647b01a6d4907141de3fc28d5ebabc095d8d8328bfb903da02cf3d8faedd337303afe8bf139e514ed7c0228e6e64bdf628cc40e359b6 languageName: node linkType: hard -"@parcel/codeframe@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/codeframe@npm:2.10.3" +"@parcel/codeframe@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/codeframe@npm:2.11.0" dependencies: chalk: ^4.1.0 - checksum: b8808789dc452079ce9a9d93b14e39713e4dc831ddec5a3bc69284632e347d6fb5714e3e4890de081f3cad2c3905643f9f95d88b1262744bb1967e3278b85842 - languageName: node - linkType: hard - -"@parcel/compressor-raw@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/compressor-raw@npm:2.10.3" - dependencies: - "@parcel/plugin": 2.10.3 - checksum: bf92cd0690ff8858bb5fea7b70bb659a77c091aa95a399ce09b67345ab87db74e6974b6ab9faad18c0deb12416da981f59e171e346291cb2a55fc3830dc0153e - languageName: node - linkType: hard - -"@parcel/config-default@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/config-default@npm:2.10.3" - dependencies: - "@parcel/bundler-default": 2.10.3 - "@parcel/compressor-raw": 2.10.3 - "@parcel/namer-default": 2.10.3 - "@parcel/optimizer-css": 2.10.3 - "@parcel/optimizer-htmlnano": 2.10.3 - "@parcel/optimizer-image": 2.10.3 - "@parcel/optimizer-svgo": 2.10.3 - "@parcel/optimizer-swc": 2.10.3 - "@parcel/packager-css": 2.10.3 - "@parcel/packager-html": 2.10.3 - "@parcel/packager-js": 2.10.3 - "@parcel/packager-raw": 2.10.3 - "@parcel/packager-svg": 2.10.3 - "@parcel/packager-wasm": 2.10.3 - "@parcel/reporter-dev-server": 2.10.3 - "@parcel/resolver-default": 2.10.3 - "@parcel/runtime-browser-hmr": 2.10.3 - "@parcel/runtime-js": 2.10.3 - "@parcel/runtime-react-refresh": 2.10.3 - "@parcel/runtime-service-worker": 2.10.3 - "@parcel/transformer-babel": 2.10.3 - "@parcel/transformer-css": 2.10.3 - "@parcel/transformer-html": 2.10.3 - "@parcel/transformer-image": 2.10.3 - "@parcel/transformer-js": 2.10.3 - "@parcel/transformer-json": 2.10.3 - "@parcel/transformer-postcss": 2.10.3 - "@parcel/transformer-posthtml": 2.10.3 - "@parcel/transformer-raw": 2.10.3 - "@parcel/transformer-react-refresh-wrap": 2.10.3 - "@parcel/transformer-svg": 2.10.3 - peerDependencies: - "@parcel/core": ^2.10.3 - checksum: 7dc834abfe81fecfa582ffeb37471328382bae1f7b3d1a45229766cdafd780b349d6b598ef2a550f72e5877726c759866b83c5b4d494a2723e8bd46910bb4b8c - languageName: node - linkType: hard - -"@parcel/core@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/core@npm:2.10.3" + checksum: abd6b7b444dadcf0a986129c782286fe77098dad584ecc91cabf0b64468b2c7150587ce5e1f548ca9943c3a12bf8fa9abddab3f193df057600bdf88b92928897 + languageName: node + linkType: hard + +"@parcel/compressor-raw@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/compressor-raw@npm:2.11.0" + dependencies: + "@parcel/plugin": 2.11.0 + checksum: 90882f3afa36c4a9f66be7379d39f144d55642398ac50b96394e91c72f2c1b1068297e12ab31f5e229645445024ded24471ee4b4ff6d754c60cf75b8f44b8c61 + languageName: node + linkType: hard + +"@parcel/config-default@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/config-default@npm:2.11.0" + dependencies: + "@parcel/bundler-default": 2.11.0 + "@parcel/compressor-raw": 2.11.0 + "@parcel/namer-default": 2.11.0 + "@parcel/optimizer-css": 2.11.0 + "@parcel/optimizer-htmlnano": 2.11.0 + "@parcel/optimizer-image": 2.11.0 + "@parcel/optimizer-svgo": 2.11.0 + "@parcel/optimizer-swc": 2.11.0 + "@parcel/packager-css": 2.11.0 + "@parcel/packager-html": 2.11.0 + "@parcel/packager-js": 2.11.0 + "@parcel/packager-raw": 2.11.0 + "@parcel/packager-svg": 2.11.0 + "@parcel/packager-wasm": 2.11.0 + "@parcel/reporter-dev-server": 2.11.0 + "@parcel/resolver-default": 2.11.0 + "@parcel/runtime-browser-hmr": 2.11.0 + "@parcel/runtime-js": 2.11.0 + "@parcel/runtime-react-refresh": 2.11.0 + "@parcel/runtime-service-worker": 2.11.0 + "@parcel/transformer-babel": 2.11.0 + "@parcel/transformer-css": 2.11.0 + "@parcel/transformer-html": 2.11.0 + "@parcel/transformer-image": 2.11.0 + "@parcel/transformer-js": 2.11.0 + "@parcel/transformer-json": 2.11.0 + "@parcel/transformer-postcss": 2.11.0 + "@parcel/transformer-posthtml": 2.11.0 + "@parcel/transformer-raw": 2.11.0 + "@parcel/transformer-react-refresh-wrap": 2.11.0 + "@parcel/transformer-svg": 2.11.0 + peerDependencies: + "@parcel/core": ^2.11.0 + checksum: b35be2dcd3e34185d43f168ad95db6c9681b8600d4e5b08768931c845479a0b2b4b360650018073c300767416dc446e5bcf0ce311e5b31c1dfa4c0d7d536a040 + languageName: node + linkType: hard + +"@parcel/core@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/core@npm:2.11.0" dependencies: "@mischnic/json-sourcemap": ^0.1.0 - "@parcel/cache": 2.10.3 - "@parcel/diagnostic": 2.10.3 - "@parcel/events": 2.10.3 - "@parcel/fs": 2.10.3 - "@parcel/graph": 3.0.3 - "@parcel/logger": 2.10.3 - "@parcel/package-manager": 2.10.3 - "@parcel/plugin": 2.10.3 - "@parcel/profiler": 2.10.3 - "@parcel/rust": 2.10.3 + "@parcel/cache": 2.11.0 + "@parcel/diagnostic": 2.11.0 + "@parcel/events": 2.11.0 + "@parcel/fs": 2.11.0 + "@parcel/graph": 3.1.0 + "@parcel/logger": 2.11.0 + "@parcel/package-manager": 2.11.0 + "@parcel/plugin": 2.11.0 + "@parcel/profiler": 2.11.0 + "@parcel/rust": 2.11.0 "@parcel/source-map": ^2.1.1 - "@parcel/types": 2.10.3 - "@parcel/utils": 2.10.3 - "@parcel/workers": 2.10.3 + "@parcel/types": 2.11.0 + "@parcel/utils": 2.11.0 + "@parcel/workers": 2.11.0 abortcontroller-polyfill: ^1.1.9 base-x: ^3.0.8 browserslist: ^4.6.6 @@ -3998,369 +4005,370 @@ __metadata: msgpackr: ^1.9.9 nullthrows: ^1.1.1 semver: ^7.5.2 - checksum: 9d9def613a19b893c56a6fbf09df07a5abd483a545c4643d8584337c828d2da2e32e56b3a1962b81719da7f384160f1383b911383860755951475a59369efa24 + checksum: 22d4e1f3f5bcb7647c520fdc90bca725cc291e8b1aea71dd1eb0ecf4deb191ccc16f07a4df660ebc237e17f5fcfe35c1f8ec1eb47189c9d953ff59eb27fde344 languageName: node linkType: hard -"@parcel/diagnostic@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/diagnostic@npm:2.10.3" +"@parcel/diagnostic@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/diagnostic@npm:2.11.0" dependencies: "@mischnic/json-sourcemap": ^0.1.0 nullthrows: ^1.1.1 - checksum: 4223a534db21a18fb47c79a9a10ca49b84bddb1135bef8772808af9f36b695d789460d103f0566902df849c8e419ef60f66ab538c597fc38e5643e717c3eb042 + checksum: bcfd74db92bb4a05eaf2abec7ade76f68b119cf3f07ab6eebc8a9b37871ef9839a7dad9476d7c114a6ce15d1fc2569de50dccd38ed28251375f0f72ad35b0349 languageName: node linkType: hard -"@parcel/events@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/events@npm:2.10.3" - checksum: f8b6c53a24378f02ccb8e5beb7f47d70a2b1e25f12541cf0e49be6daf998001806f9da862e908be291e9b44d880b387eee8b2790a7abf22bbefaafc07bd1664d +"@parcel/events@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/events@npm:2.11.0" + checksum: e757383143ad2c5b5fed800eb8fd8a0bf2d6c362dbc1f55993d0be575b5f7b7ad0fcead1db695ceb48e49fb13126849b282879097b4a14ccb8fc3fc522e9e10f languageName: node linkType: hard -"@parcel/fs@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/fs@npm:2.10.3" +"@parcel/fs@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/fs@npm:2.11.0" dependencies: - "@parcel/rust": 2.10.3 - "@parcel/types": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/rust": 2.11.0 + "@parcel/types": 2.11.0 + "@parcel/utils": 2.11.0 "@parcel/watcher": ^2.0.7 - "@parcel/workers": 2.10.3 + "@parcel/workers": 2.11.0 peerDependencies: - "@parcel/core": ^2.10.3 - checksum: 916a7a0d6b3c98aa089d85445971f4fb6bf90ef94fb5d941e0c27d4cb775309027a537b5c144352299eadc516fe30f681c3843a24a4e518a24074c25830819c0 + "@parcel/core": ^2.11.0 + checksum: 2702d29dd72c73b30268b1dbeba5bd8c74638baa69c96415c9fc5182cf3fa312c575f22e01f021f42043b32a877ca980e61997332301b15fc2fa626e485edd13 languageName: node linkType: hard -"@parcel/graph@npm:3.0.3": - version: 3.0.3 - resolution: "@parcel/graph@npm:3.0.3" +"@parcel/graph@npm:3.1.0": + version: 3.1.0 + resolution: "@parcel/graph@npm:3.1.0" dependencies: nullthrows: ^1.1.1 - checksum: f906c7709b37abb5e8fb47919de2a1b763a0ee2851fe42f924d16a27f98435a92f0520c1c9990d31d9c835cbd25e9eeb57447ee4b9e8b56556857ad1fb28495a + checksum: 6d645e3ab4bb06fbc967e71072ebe796c602aacf5ac79d30e2d40442d6b08785bed49165b28a914ca419a949f4dec4dc3b2557520064c4a53bcc04f4b023daf0 languageName: node linkType: hard -"@parcel/logger@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/logger@npm:2.10.3" +"@parcel/logger@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/logger@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/events": 2.10.3 - checksum: f67f4010122b6b2e65d9789d328e0672cb250d7e0bf66528c317ddd8d95a771c67c87d75d049b5ed6b0b5858c860052301e9418faeee0c7f3d27e79d688874f5 + "@parcel/diagnostic": 2.11.0 + "@parcel/events": 2.11.0 + checksum: 363cacf5969df4d8b28d397f025cb9f318ebd940cd2372ffdcd25bdbeca25844b9ae7777f35dd7cdb2d1029a8c7379f6c0173bdfc201b88f4acfe6fd7f315ec5 languageName: node linkType: hard -"@parcel/markdown-ansi@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/markdown-ansi@npm:2.10.3" +"@parcel/markdown-ansi@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/markdown-ansi@npm:2.11.0" dependencies: chalk: ^4.1.0 - checksum: e683f8ce1dd12f2a414e3f7840de3a46f0477c4dbf603dfa9b49372367bf248d115b2c73fbba85ad0dff27bd156a55761aa22d12afba098197dc3c7a4f9e6730 + checksum: 26bc69d6e227b3d461b3501014bd9c33cdb7c6514447b7eeb0f17c97ff9dc74f3bdddf271f9976e14c918516044128f477e884ce94b765c5a7e23661edfa739f languageName: node linkType: hard -"@parcel/namer-default@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/namer-default@npm:2.10.3" +"@parcel/namer-default@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/namer-default@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 nullthrows: ^1.1.1 - checksum: ea6adf6c5cb44d97ce929b967b6ebf5d383c907284166ee57554a8d3815b7da42a867202e8f69ee190de35287639e2ed74f8cd540ecf232232dcc8423a3a613a + checksum: 452978cb597729cc21aec11456dd2121860893c9dec20f588cfc087643f127a6553493bcec5916d32285e7af11d5b832c24f07a10424cf264b4dac7ba1dfc101 languageName: node linkType: hard -"@parcel/node-resolver-core@npm:3.1.3": - version: 3.1.3 - resolution: "@parcel/node-resolver-core@npm:3.1.3" +"@parcel/node-resolver-core@npm:3.2.0": + version: 3.2.0 + resolution: "@parcel/node-resolver-core@npm:3.2.0" dependencies: "@mischnic/json-sourcemap": ^0.1.0 - "@parcel/diagnostic": 2.10.3 - "@parcel/fs": 2.10.3 - "@parcel/rust": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/fs": 2.11.0 + "@parcel/rust": 2.11.0 + "@parcel/utils": 2.11.0 nullthrows: ^1.1.1 semver: ^7.5.2 - checksum: 7daa061d8170eee165005686b9ac82823a5fda5465246226bd8ca163b9434ebfa70163bf64b103f56e0fe7d6b72f10ebf24e0bcc2f832e258876484d0707636e + checksum: 9a006b50b1af5d0c4461647d5de5932a39c910ddd8da478dba68d4c814a92cb20cfca59fde98a3def9b899126a637bb7e1445fa97e794cb57487e7c0be678858 languageName: node linkType: hard -"@parcel/optimizer-css@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/optimizer-css@npm:2.10.3" +"@parcel/optimizer-css@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/optimizer-css@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 "@parcel/source-map": ^2.1.1 - "@parcel/utils": 2.10.3 + "@parcel/utils": 2.11.0 browserslist: ^4.6.6 - lightningcss: ^1.16.1 + lightningcss: ^1.22.1 nullthrows: ^1.1.1 - checksum: f157c14136b24bace773d721d29192dd913770dea3b65348993ce0bd6dbdeb186c4a4bfc42b6912a4674814d4ed73761568bd976e09f1d988a66095d8da83920 + checksum: 8407b5e864a96e768a719ff35498353c5604c8b0c469f2a265d133bce05a1e80a0d95adcfc0a758c83ff35da5b3d12bf44b0b0f8598d2e2c16e86e6c527e9208 languageName: node linkType: hard -"@parcel/optimizer-htmlnano@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/optimizer-htmlnano@npm:2.10.3" +"@parcel/optimizer-htmlnano@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/optimizer-htmlnano@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 + "@parcel/plugin": 2.11.0 htmlnano: ^2.0.0 nullthrows: ^1.1.1 posthtml: ^0.16.5 svgo: ^2.4.0 - checksum: d2d7c4d5596e64b5a1ee81824aabb132853fbd065e2e8816e061702872ec4cec3527d9f63206d20d78176a4d1e075f25d5f90590b6540b8aa57f0295c59c687b + checksum: 4943948a8f345b8f7024d68b7f401befe943f0de00eb826df530e67f8155739dbae386fae350ad6e7a8ec00ba41598333fa48a3e51cc364cd517e17975131f38 languageName: node linkType: hard -"@parcel/optimizer-image@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/optimizer-image@npm:2.10.3" +"@parcel/optimizer-image@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/optimizer-image@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 - "@parcel/rust": 2.10.3 - "@parcel/utils": 2.10.3 - "@parcel/workers": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 + "@parcel/rust": 2.11.0 + "@parcel/utils": 2.11.0 + "@parcel/workers": 2.11.0 peerDependencies: - "@parcel/core": ^2.10.3 - checksum: 40a4132b9edc344fcab6607b69a3b05743ad1478319183b5eb54c3ee21d676eb9ecdd78ada676998a7cd3a1daaf0cb02c65504cbe9343001e10fd7fff7d1b03d + "@parcel/core": ^2.11.0 + checksum: 9c4d9d087fd9bb47c150b80e2f92620ba376ca6462415a17ea0d81a0c674db363fe8364a7fc39aa1bed25c96e95742b73e18fdab1f2767c78b6e6f9c0fbbe08c languageName: node linkType: hard -"@parcel/optimizer-svgo@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/optimizer-svgo@npm:2.10.3" +"@parcel/optimizer-svgo@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/optimizer-svgo@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 + "@parcel/utils": 2.11.0 svgo: ^2.4.0 - checksum: f26c8c931f0648b6c908e6d9639be010cb3f0551eaaf46f205bba39fcdb5190a20a094847f91036734a0b9749bf607ae22b0bedbecaebe30fa198965523fc228 + checksum: 4e671856c28435e4f19335fa5ed062878a9d6fe4232ff22befab5a90df360db1904afbd55bfd4ecff193f4bcd7a9081f5d23db54bb644bb7160fcfa1aac4facf languageName: node linkType: hard -"@parcel/optimizer-swc@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/optimizer-swc@npm:2.10.3" +"@parcel/optimizer-swc@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/optimizer-swc@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 "@parcel/source-map": ^2.1.1 - "@parcel/utils": 2.10.3 + "@parcel/utils": 2.11.0 "@swc/core": ^1.3.36 nullthrows: ^1.1.1 - checksum: 6eb1e2c923cff94f71a3d5a8218f826440ac7c0162cc45d9b27ae20ad62f1ab0dcd5efea4cbabebb3cbfe0d6d56b26693754e5daca8469ab36914d7ece1be3ba + checksum: be4b8bc94a5a490f6912a6dee24473eced904f2122f684d164cd781e7b216375999c5a15efa91fece4cb50cfc0c4eaabe9a8773df263163b5951e9d4e76a5798 languageName: node linkType: hard -"@parcel/package-manager@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/package-manager@npm:2.10.3" +"@parcel/package-manager@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/package-manager@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/fs": 2.10.3 - "@parcel/logger": 2.10.3 - "@parcel/node-resolver-core": 3.1.3 - "@parcel/types": 2.10.3 - "@parcel/utils": 2.10.3 - "@parcel/workers": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/fs": 2.11.0 + "@parcel/logger": 2.11.0 + "@parcel/node-resolver-core": 3.2.0 + "@parcel/types": 2.11.0 + "@parcel/utils": 2.11.0 + "@parcel/workers": 2.11.0 semver: ^7.5.2 peerDependencies: - "@parcel/core": ^2.10.3 - checksum: d25958e342cd6886b7d92b12100f8f58dadc80c925a28e25b8a7aa7349955d701c927b42e97ecff8b078452ff92dd4b976782b47fda7268533e9412de86c710f + "@parcel/core": ^2.11.0 + checksum: 3846542eb59ee39ad53c1aea56f59045a26e58d07b0f0f59a91ca1b91e1268e76cd564a914e1041120f870481ae46063a39737e67d2418547cc0ff0dcf08a124 languageName: node linkType: hard -"@parcel/packager-css@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/packager-css@npm:2.10.3" +"@parcel/packager-css@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/packager-css@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 "@parcel/source-map": ^2.1.1 - "@parcel/utils": 2.10.3 + "@parcel/utils": 2.11.0 nullthrows: ^1.1.1 - checksum: 5da9263a0fcf233c0793029518220a8bbcc4f8707196c1a85acfbf96ebfb821fdc4da2ce01af92b16178e6db4e214d627fc61902fecd65910c1dcecb6af60273 + checksum: 2aa8da12ca83353153cb4d70893dfa691bab6f9e884b434812f3d4200fec3d2bd5a89df1a4c6c9a0d0955b5b5ef88bfacb4287f87c066f377fc304ef5a099720 languageName: node linkType: hard -"@parcel/packager-html@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/packager-html@npm:2.10.3" +"@parcel/packager-html@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/packager-html@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/types": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/plugin": 2.11.0 + "@parcel/types": 2.11.0 + "@parcel/utils": 2.11.0 nullthrows: ^1.1.1 posthtml: ^0.16.5 - checksum: 85bfdba8be5be33f37c520cc6b770151ab7ad263032aff1126a64a1df9676f25478f7d3e67ea76374362287bbed3419939ae0e6d576b2a20f60f51a23f24fa58 + checksum: a651b4f3cf1e398ff111f31519610957863701471a763369fa26fa7398ed96c02a41f76575e60262e091506f7e7b4c7d3c7df9b3a12fb04dc62e7c9fe0604d71 languageName: node linkType: hard -"@parcel/packager-js@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/packager-js@npm:2.10.3" +"@parcel/packager-js@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/packager-js@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 - "@parcel/rust": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 + "@parcel/rust": 2.11.0 "@parcel/source-map": ^2.1.1 - "@parcel/types": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/types": 2.11.0 + "@parcel/utils": 2.11.0 globals: ^13.2.0 nullthrows: ^1.1.1 - checksum: 1e383bdf39c495d23ac52acb4a821ef9d405ae4eee160a9571dee005faa60e1404046c59250c99898691da6fffa18e9dee5e317b6d06ee7503717ebbb4f7e59f + checksum: 63d96fb22be90f54f5562fc93baad13deff63d7e434648f606b455800610679231a8e7b1349bd0f212568e2e4cf519c6368fbda8f1b0ae392870ecfa6bf8265f languageName: node linkType: hard -"@parcel/packager-raw@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/packager-raw@npm:2.10.3" +"@parcel/packager-raw@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/packager-raw@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - checksum: 7f6615fa93c18e14014de07e677587ccfd1e275972d577c3ef4359969f8fca4ec12351be04a0d34e6287b63041259a0b31bc239555b7cdff7cfec734667f6e4f + "@parcel/plugin": 2.11.0 + checksum: c36dbcc82a31643fd83dce8a5dc5bb038a9396ffec46a8f9c8a7ad35f4645d2050c4228ed360f2c70f11073a606b6c89872a70883d07594b91bd8ce9191ed3a2 languageName: node linkType: hard -"@parcel/packager-svg@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/packager-svg@npm:2.10.3" +"@parcel/packager-svg@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/packager-svg@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/types": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/plugin": 2.11.0 + "@parcel/types": 2.11.0 + "@parcel/utils": 2.11.0 posthtml: ^0.16.4 - checksum: fcc64795be57a3c32e7861d424d3aaaa585cda126a0f7e03d28ffb4cd2105218ae4d1ce8ac68cb986a8fe5b77adf80722de18b6af4d094f368c810aff346f58c + checksum: 2d0394dd5e9ce0025f805241975ee26318f176b42ca30d6f020cfb5521f77ca6eb2b13c9c23f580bab69e55ca883f06d6c023faa3b84e15f2e7d6e826ed2f961 languageName: node linkType: hard -"@parcel/packager-wasm@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/packager-wasm@npm:2.10.3" +"@parcel/packager-wasm@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/packager-wasm@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - checksum: 971a8b1ef763fb593d80536b7728545bc471208fecb05344fb5b4af7f38dc9f5f5faad8047388d4bfcff8bba7b96cf1437789e017e40b0fd95a6da99b03db942 + "@parcel/plugin": 2.11.0 + checksum: 4f0617fe512ffdce0157dff47311ef241645482ddd8a90614bab658de7c1b51387eede6e841480c4478557934e2ce1d318d970f45a8048edfbf8992da7c2bc18 languageName: node linkType: hard -"@parcel/plugin@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/plugin@npm:2.10.3" +"@parcel/plugin@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/plugin@npm:2.11.0" dependencies: - "@parcel/types": 2.10.3 - checksum: d5d18e815856bf76d4e5a6b3daaefa21030a67e46b38cebb9ee4d4aa98b352bce3f54d8c3fc4136692c31e85ca23f1366bd86f77c78cd0daeacbad82a59068d0 + "@parcel/types": 2.11.0 + checksum: 45344c3ac0674b77ea29cbac23de5887fac68bfba118adf93c9bab4631c1e341e1212d250d1279581c51ad6124b6f023bbb5c2ca0e7c1afed4442fc0a0398a97 languageName: node linkType: hard -"@parcel/profiler@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/profiler@npm:2.10.3" +"@parcel/profiler@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/profiler@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/events": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/events": 2.11.0 chrome-trace-event: ^1.0.2 - checksum: b6d797c7abddb2e8861a77fc3f8e11d329a41e3257ba6d5eddfb5cb158d3299d380d49114dfccac54ce73c9ba82e779a39fe3f0e62b40aae4eb8ade5ed3e8726 + checksum: f5b5749786de62f1d9d0285adbbe49e170e691fdbf789d9d706c84b72ed6a300978f2144f7c56f4695fff6ee4388df97158554b105631adde6e43d3ee0090784 languageName: node linkType: hard -"@parcel/reporter-cli@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/reporter-cli@npm:2.10.3" +"@parcel/reporter-cli@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/reporter-cli@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/types": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/plugin": 2.11.0 + "@parcel/types": 2.11.0 + "@parcel/utils": 2.11.0 chalk: ^4.1.0 + cli-progress: ^3.12.0 term-size: ^2.2.1 - checksum: 7b6f4c0e964955422a20936ca53bb864fe0e1a9729a68e303ab9daa6ead8fabb48b695614852549357a881734a230980967d7e1c76f2f78b279e14e28725f2ed + checksum: 81e62144ad834791656458eeea286a061f4b8fa4651d8c68e54ab8050f9101ee9cfc9117f18544baebea5d47da9f4b2e3e6eabc19e7c886131bdcd9b2f40dd71 languageName: node linkType: hard -"@parcel/reporter-dev-server@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/reporter-dev-server@npm:2.10.3" +"@parcel/reporter-dev-server@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/reporter-dev-server@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/utils": 2.10.3 - checksum: 0dd35076154e705ea0f00ffbb7fd452a7fd98d0c13cf9af2b72a8762509dfe87594c00e0cfd886ee6d2a4134379581eb281f5e1fde4eb4b4360fb547bc1f3db9 + "@parcel/plugin": 2.11.0 + "@parcel/utils": 2.11.0 + checksum: ac8948c7f30a88c8d05d3a168381d906e6b6bfbdf04001ec46a57b0f9901f009d111ca7d200e5079ca34ce23b7754be38902492ffacdd42d544cbe26c7c2dc67 languageName: node linkType: hard -"@parcel/reporter-tracer@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/reporter-tracer@npm:2.10.3" +"@parcel/reporter-tracer@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/reporter-tracer@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/plugin": 2.11.0 + "@parcel/utils": 2.11.0 chrome-trace-event: ^1.0.3 nullthrows: ^1.1.1 - checksum: 818ff5a49c836fc965ebd21373c0924453ae9b5fe2f9bde5e4735cb1bdd16db81af65f2f48235a95e34c9bacec6081b29bf612dc0e3e4d1db7811bec91ac1b2d + checksum: 83aedfc97d3b79f5aae205c88267fe6baf59ccee211aca251198c55594760e84145a3e070ce5efcbef648b72c752649283f13b5500fb848f83dbf723dff295df languageName: node linkType: hard -"@parcel/resolver-default@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/resolver-default@npm:2.10.3" +"@parcel/resolver-default@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/resolver-default@npm:2.11.0" dependencies: - "@parcel/node-resolver-core": 3.1.3 - "@parcel/plugin": 2.10.3 - checksum: 4c6025a129fbdb42964c84e58df3dc555bb77dae2d134a6e98de652e14de1c1b600766add5e95ccaffb3c7bfc006111fdb9aabeb96f815c875c9e3846d66e484 + "@parcel/node-resolver-core": 3.2.0 + "@parcel/plugin": 2.11.0 + checksum: bbb5f6dfd67c754680dade42005a6ddc5fc5d40f7277ba1d63df898ac117ad98c2d74b599b2df91c7f2f8503bf6ba3cb10eef9d9b4d9c09ef0e8a7eb98d38d6e languageName: node linkType: hard -"@parcel/runtime-browser-hmr@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/runtime-browser-hmr@npm:2.10.3" +"@parcel/runtime-browser-hmr@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/runtime-browser-hmr@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/utils": 2.10.3 - checksum: 6977acaf7797e070626667bef7849cfaebad89323c2da718c690f05f44b778bba7045469d704e7995fe06fd4e4f77194a8059218c85bcb186a05c753c0cf4c7c + "@parcel/plugin": 2.11.0 + "@parcel/utils": 2.11.0 + checksum: ad7eba3210227ebbcb71889cdfb495172ae0173b75e140b41fe772df90c115f1bab5c23fd6209eabe94f143942725f6b3a8d497483e376cf449078c0a984bf1c languageName: node linkType: hard -"@parcel/runtime-js@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/runtime-js@npm:2.10.3" +"@parcel/runtime-js@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/runtime-js@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 + "@parcel/utils": 2.11.0 nullthrows: ^1.1.1 - checksum: a0c3f95eefbd9e6a9678d838bedac997ef4ebb797c1aef25d2e72217a70fdf54ae44a1ef0ff4c0161e0df004ee6a44b66958edfd5df9f95cd2017f049f0be2f0 + checksum: 3cab5e6b6218b91b9022c9e0990674be46abd763dd4f5deaaa4d1143df22a37f479d2a1f41f9b1be35e1381437c3ed301b03afec62bb8cba613e473ffd22d43c languageName: node linkType: hard -"@parcel/runtime-react-refresh@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/runtime-react-refresh@npm:2.10.3" +"@parcel/runtime-react-refresh@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/runtime-react-refresh@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/plugin": 2.11.0 + "@parcel/utils": 2.11.0 react-error-overlay: 6.0.9 react-refresh: ^0.9.0 - checksum: 00f69e6a57918c628fdd4ee2dc4b65e81bdc0be15074d32ffdd6db3aa6d00b0ce0a4e005aba028fc0deaccf38f8b319bf0b5900999cbca5530ad29ab7a0a4074 + checksum: 0a29d9e55ad296ae13413e13ecea2e5966edc4e26a542bf817cc72cd15f793930abe7b2e80065e39ee647be13e211951146c8c643184da8fa2af872f09e9ffc5 languageName: node linkType: hard -"@parcel/runtime-service-worker@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/runtime-service-worker@npm:2.10.3" +"@parcel/runtime-service-worker@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/runtime-service-worker@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/plugin": 2.11.0 + "@parcel/utils": 2.11.0 nullthrows: ^1.1.1 - checksum: 01370853dfc535804353169f349b1d7a8291f4daa0b7728c052c1d9759e4b338fced1801490d8dd6aa12816135f475124daa9c747f7ba68c7c899064b3abb88f + checksum: da8d2b5a925f710e14f9dd49d968603946961b4f8d1f4ef8d4b68e350118d2f6ae42115adcc6952ebe01151dd185de827902d3984367c20c5f9381890d8bfeb5 languageName: node linkType: hard -"@parcel/rust@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/rust@npm:2.10.3" - checksum: 5c1a086742b0a53c935bc992a71cadc8fc3f5bd7ce7fce5dcf13a4079a867ef6df09422cd668e44c3cec0be19630441ca11c95081fc922287312893c5c0ebe1a +"@parcel/rust@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/rust@npm:2.11.0" + checksum: 53a95b23562d71597af726647923db08185ede5b83fd25f00467229b62a51fd5b43a0e44172d90d68721e1463fb4c5f06000ddd47d402779177fe7c441fe8b16 languageName: node linkType: hard @@ -4373,194 +4381,194 @@ __metadata: languageName: node linkType: hard -"@parcel/transformer-babel@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-babel@npm:2.10.3" +"@parcel/transformer-babel@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-babel@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 "@parcel/source-map": ^2.1.1 - "@parcel/utils": 2.10.3 + "@parcel/utils": 2.11.0 browserslist: ^4.6.6 json5: ^2.2.0 nullthrows: ^1.1.1 semver: ^7.5.2 - checksum: 5dc31a9ff4b4f7a1400a16c2f2d44badcf330dfc98ff935ece813ce9efd926d34bc230a60c146bd3bb43b2da675f1289b3b55b8386f2739abb521e5c34970d59 + checksum: f36c7973c691619b4c35bdd40135cd6daf735b333f34df99b6cad1b0ad332010b195bdb0388a6ff8b6abad201020a77ad797b974774bb0d26894d1c435f30814 languageName: node linkType: hard -"@parcel/transformer-css@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-css@npm:2.10.3" +"@parcel/transformer-css@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-css@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 "@parcel/source-map": ^2.1.1 - "@parcel/utils": 2.10.3 + "@parcel/utils": 2.11.0 browserslist: ^4.6.6 - lightningcss: ^1.16.1 + lightningcss: ^1.22.1 nullthrows: ^1.1.1 - checksum: f9cf5f04ceae726cf8a5c95ba326a73125423245fe34317593dde9956bead1e48021254ffffd0b5b41cd8f71a953580e1216d0897557c7d0999f360a7797ad43 + checksum: 59b1893b94388b4e7e94121cd82cbf734499e1565503b91ec0ea837d9967190c953323a4c054d8d80fd5c53f5b75710a5f20f54d129a076257497a269316a280 languageName: node linkType: hard -"@parcel/transformer-html@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-html@npm:2.10.3" +"@parcel/transformer-html@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-html@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 - "@parcel/rust": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 + "@parcel/rust": 2.11.0 nullthrows: ^1.1.1 posthtml: ^0.16.5 posthtml-parser: ^0.10.1 posthtml-render: ^3.0.0 semver: ^7.5.2 srcset: 4 - checksum: ecf9f0c18791bdb1b2c65abbe8ef1fd0fe6bb33d4831c294efc78cf8094e7444e1746b86ad82e2cb7fec02c7c94fd8c0a6190201a12ff83e22b6d243cd665ece + checksum: fd333e9a4890bb565b0403f065a46c9c38174b379f6fc30b557455fad13d9b13864bf2cb971ff396cea8724c74c652336f727e2df260e2bfb6d2ea7d952371ae languageName: node linkType: hard -"@parcel/transformer-image@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-image@npm:2.10.3" +"@parcel/transformer-image@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-image@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/utils": 2.10.3 - "@parcel/workers": 2.10.3 + "@parcel/plugin": 2.11.0 + "@parcel/utils": 2.11.0 + "@parcel/workers": 2.11.0 nullthrows: ^1.1.1 peerDependencies: - "@parcel/core": ^2.10.3 - checksum: 5e1fc73ca2e38f0fcb67cc0851e27a2d0f1bcb01c5b6f6e0ba8195987c32a26111cd27ba26d6d9f0374773c04f98dd88e1e4fcec770a596f0618b4a9bae3e46f + "@parcel/core": ^2.11.0 + checksum: 65e7fa4791100b5db6dd6752a0f43c2c377e2f3825c5cfeb8bff5d3ae994e3ac659ebcb656a607834f54eb3cf6cf499ed78fdba64a968428c2bbcdb23b030d19 languageName: node linkType: hard -"@parcel/transformer-js@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-js@npm:2.10.3" +"@parcel/transformer-js@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-js@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 - "@parcel/rust": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 + "@parcel/rust": 2.11.0 "@parcel/source-map": ^2.1.1 - "@parcel/utils": 2.10.3 - "@parcel/workers": 2.10.3 + "@parcel/utils": 2.11.0 + "@parcel/workers": 2.11.0 "@swc/helpers": ^0.5.0 browserslist: ^4.6.6 nullthrows: ^1.1.1 regenerator-runtime: ^0.13.7 semver: ^7.5.2 peerDependencies: - "@parcel/core": ^2.10.3 - checksum: 7d534ea520808a3f12fa0751f88034a6212e0ec876934b570a48bd905ddea20d1fd8d543e23fd6fa4674591ccad476f1495e7b9732209a22d287674b82df6b02 + "@parcel/core": ^2.11.0 + checksum: ae75820a392f9728ecd4fe98a9ec746c380fb2fd927845980d0f95b0e89afe8c9bf557ba66006612811bc362caa07cd4697dfa732df8cf0de711e8666f0c80ee languageName: node linkType: hard -"@parcel/transformer-json@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-json@npm:2.10.3" +"@parcel/transformer-json@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-json@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 + "@parcel/plugin": 2.11.0 json5: ^2.2.0 - checksum: f08d2a13b284484e4d062b6650d65329d0da1da04dadd770da9911a44b40a051e745f80b5b61e82ad74d99816c7cb10e0094144b5b033cb17c85d38a916f4714 + checksum: 6e25561d09ea238e96558a76c0812c423ca44633824b3271fb3bc5b77604aefe8943332f2b7b17ff7a296cc6b770a6ba3d2322eeac4650785c2f091314c71ddf languageName: node linkType: hard -"@parcel/transformer-postcss@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-postcss@npm:2.10.3" +"@parcel/transformer-postcss@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-postcss@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 - "@parcel/rust": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 + "@parcel/rust": 2.11.0 + "@parcel/utils": 2.11.0 clone: ^2.1.1 nullthrows: ^1.1.1 postcss-value-parser: ^4.2.0 semver: ^7.5.2 - checksum: bcea34678d39231cd91cebdc8790b4b9f6e56b9a8c94cadd54ab90b43d282505fe17c8b5414b0fdf82d7b66620bffc587c21f2613356085cba7e7dfc5badea77 + checksum: 10f0054e5b751275fc1571d32d91f4412b31575d45eb576d1483771c3d3c445bd50da7937a8aa6b0aa134212e2f3b608482a6e617e6472f77694cd52ec413c82 languageName: node linkType: hard -"@parcel/transformer-posthtml@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-posthtml@npm:2.10.3" +"@parcel/transformer-posthtml@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-posthtml@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/plugin": 2.11.0 + "@parcel/utils": 2.11.0 nullthrows: ^1.1.1 posthtml: ^0.16.5 posthtml-parser: ^0.10.1 posthtml-render: ^3.0.0 semver: ^7.5.2 - checksum: afa94afee4b0e1769227bbd6a8928bd5ce8c38711bc8164cb9a76af2134643d28c3b1887a8237b9c2572f0a30f7181aaeb3ed3c1a4882b064a3f30e2f145b77d + checksum: d79e3666cae4ffb648d042b30e974d68193c05ffb6418d0c79fe784e07b323fe65a3006130f63eeafe0700a09300d8c9e6dcf6a56e24ac2a23fdd30d3bd4a9b5 languageName: node linkType: hard -"@parcel/transformer-raw@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-raw@npm:2.10.3" +"@parcel/transformer-raw@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-raw@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - checksum: 26f5b5c1f80f60b6ea404cce9372591f643accc3357b7164b208888d47e7b9731433d3ef4e62034cadbc998ef84cfa0d9e6b63a842e0f09157d3f5d46324342f + "@parcel/plugin": 2.11.0 + checksum: b914665c090316d677fedc1b646b87c31eea277fb26281b737874940fe1c63bdbcf0f31dc4d2a4d5493bf4e61b420e7b4827039bd8409aaed8e8da4a5a03bacc languageName: node linkType: hard -"@parcel/transformer-react-refresh-wrap@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-react-refresh-wrap@npm:2.10.3" +"@parcel/transformer-react-refresh-wrap@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-react-refresh-wrap@npm:2.11.0" dependencies: - "@parcel/plugin": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/plugin": 2.11.0 + "@parcel/utils": 2.11.0 react-refresh: ^0.9.0 - checksum: 5959b5860d1ac342bd3eaf9fcbde2065c84ef4347100f36ae29cc322a87aea5b1175142fe6c181f9fddbc858384755787f5b9398da2078d5881f2b2f94caf19b + checksum: b17b7a151da31f6abe7e33d798205af3926135e5e936025f74883285519a9fff5207c89fd06c21635965e9d8e46d62450915623d1b5bd97d50297a6633703519 languageName: node linkType: hard -"@parcel/transformer-svg@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/transformer-svg@npm:2.10.3" +"@parcel/transformer-svg@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/transformer-svg@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/plugin": 2.10.3 - "@parcel/rust": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/plugin": 2.11.0 + "@parcel/rust": 2.11.0 nullthrows: ^1.1.1 posthtml: ^0.16.5 posthtml-parser: ^0.10.1 posthtml-render: ^3.0.0 semver: ^7.5.2 - checksum: aa1c3378fc4b809cca9dd90804030d204a587e3a4bebf5f0cf3853eaf6eff0cf82b47e304df2b532831eaabe48143a996a789d3d199f156de534ff03c68a3c8e + checksum: 48bf4503ded4328394fcb23ab08856a2b81fda4ec3e7755ec97492511324d66b20ed1f47e0588f011b0319795094e98537683634318eaf53fcd89229f7e9088c languageName: node linkType: hard -"@parcel/types@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/types@npm:2.10.3" +"@parcel/types@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/types@npm:2.11.0" dependencies: - "@parcel/cache": 2.10.3 - "@parcel/diagnostic": 2.10.3 - "@parcel/fs": 2.10.3 - "@parcel/package-manager": 2.10.3 + "@parcel/cache": 2.11.0 + "@parcel/diagnostic": 2.11.0 + "@parcel/fs": 2.11.0 + "@parcel/package-manager": 2.11.0 "@parcel/source-map": ^2.1.1 - "@parcel/workers": 2.10.3 + "@parcel/workers": 2.11.0 utility-types: ^3.10.0 - checksum: ffc6d24df95b5458e02e2a841fac6bee6ba9f78f3f0c3ae9e465a546a3a05e91ab2781850785a6e153ce20f456b9d4427ea0754cdc8efaabf2fee5823ac5f89b + checksum: 8534156cafcd8eeee008f7cdbe91a609c8e3c9d1fe9248c3649e0649379c3c5cd141a038ca06f457e6b46d254f2667fd55d1b89a3cf3f6091ea5102422c8fc41 languageName: node linkType: hard -"@parcel/utils@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/utils@npm:2.10.3" +"@parcel/utils@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/utils@npm:2.11.0" dependencies: - "@parcel/codeframe": 2.10.3 - "@parcel/diagnostic": 2.10.3 - "@parcel/logger": 2.10.3 - "@parcel/markdown-ansi": 2.10.3 - "@parcel/rust": 2.10.3 + "@parcel/codeframe": 2.11.0 + "@parcel/diagnostic": 2.11.0 + "@parcel/logger": 2.11.0 + "@parcel/markdown-ansi": 2.11.0 + "@parcel/rust": 2.11.0 "@parcel/source-map": ^2.1.1 chalk: ^4.1.0 nullthrows: ^1.1.1 - checksum: fb9aa1ee1a9de81a917a58e4f0960440597accdd68d00e775d69b5ddcf6c82ceb348f412301cfaa78560f36786283cab6105d647cd6442c6be2693b2afec61f3 + checksum: e40f4bca049881c9c4b287f0425b197f44e87a958f528e8598304b61bf645ec648ee68b4e7f194de9a3341cdd6857f3fedce71dc8ca7327d8d37017e350f42b7 languageName: node linkType: hard @@ -4698,19 +4706,19 @@ __metadata: languageName: node linkType: hard -"@parcel/workers@npm:2.10.3": - version: 2.10.3 - resolution: "@parcel/workers@npm:2.10.3" +"@parcel/workers@npm:2.11.0": + version: 2.11.0 + resolution: "@parcel/workers@npm:2.11.0" dependencies: - "@parcel/diagnostic": 2.10.3 - "@parcel/logger": 2.10.3 - "@parcel/profiler": 2.10.3 - "@parcel/types": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/diagnostic": 2.11.0 + "@parcel/logger": 2.11.0 + "@parcel/profiler": 2.11.0 + "@parcel/types": 2.11.0 + "@parcel/utils": 2.11.0 nullthrows: ^1.1.1 peerDependencies: - "@parcel/core": ^2.10.3 - checksum: 5418cfa25d22b1d786d754c20030b4aa081b62e96daf247459549df7f4971229635fa5ecb2860ee500af08a172f0eed689654ed867148c18ae0dae9aac1e68f2 + "@parcel/core": ^2.11.0 + checksum: 0771ad5372a4d27e64d6009df1a81a8a302031dcef041ddd11b43089262af3c4e6c62c0beb05485eddb2a61fc6993eb8ed04379de60cb61988d5195b352e88cc languageName: node linkType: hard @@ -7505,13 +7513,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:29.5.10": - version: 29.5.10 - resolution: "@types/jest@npm:29.5.10" +"@types/jest@npm:29.5.11": + version: 29.5.11 + resolution: "@types/jest@npm:29.5.11" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: ef385905787db528de9b6beb2688865c0bb276e64256ed60b9a1a6ffc0b75737456cb5e27e952a3241c5845b6a1da487470010dd30f3ca59c8581624c564a823 + checksum: f892a06ec9f0afa9a61cd7fa316ec614e21d4df1ad301b5a837787e046fcb40dfdf7f264a55e813ac6b9b633cb9d366bd5b8d1cea725e84102477b366df23fdd languageName: node linkType: hard @@ -7574,13 +7582,6 @@ __metadata: languageName: node linkType: hard -"@types/linkify-it@npm:*": - version: 3.0.3 - resolution: "@types/linkify-it@npm:3.0.3" - checksum: a734becc4e7476833b0e6951ec133c006a34809639c722d3e28b7cf88f5f6ccbb433f195788be5e56209b1e9e6e0778879291dd2db401acee3bb585c44dcc329 - languageName: node - linkType: hard - "@types/lodash.debounce@npm:4.0.9": version: 4.0.9 resolution: "@types/lodash.debounce@npm:4.0.9" @@ -7604,23 +7605,6 @@ __metadata: languageName: node linkType: hard -"@types/markdown-it@npm:13.0.7": - version: 13.0.7 - resolution: "@types/markdown-it@npm:13.0.7" - dependencies: - "@types/linkify-it": "*" - "@types/mdurl": "*" - checksum: c9e9af441340eb870a7b90b298f6197aa80b55bee28f179a4f85052333f0cb3d3f2763981359d58cf09024961f013999c1c743c1e52a185ca36576d4403f7eb9 - languageName: node - linkType: hard - -"@types/mdurl@npm:*": - version: 1.0.3 - resolution: "@types/mdurl@npm:1.0.3" - checksum: 5bbed4f0eb9f60040fa26be77aa2158ca468b6423876cec0d2043e7f8298e83b8e5b95fb66056327b02d747c4d376aed16c11ff3fdc4cb3dca327a6931a71f18 - languageName: node - linkType: hard - "@types/mime@npm:*": version: 3.0.2 resolution: "@types/mime@npm:3.0.2" @@ -7666,12 +7650,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:20.10.3": - version: 20.10.3 - resolution: "@types/node@npm:20.10.3" +"@types/node@npm:20.10.8": + version: 20.10.8 + resolution: "@types/node@npm:20.10.8" dependencies: undici-types: ~5.26.4 - checksum: 34a329494f0ea239af05eeb6f00f396963725b3eb9a2f79c5e6a6d37e823f2ab85e1079c2ee56723a37d8b89e7bbe2bd050c97144e5bb06dab93fd1cace65c97 + checksum: ce9b7ee545b3605f667be2ea900e38ab58d7b561192a7342443e5d7f61c44fd9d016eac48e95d3011f090ceea65a727e83a31d51fabdd9fc20ff9992edcbc682 languageName: node linkType: hard @@ -7745,12 +7729,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:18.2.17": - version: 18.2.17 - resolution: "@types/react-dom@npm:18.2.17" +"@types/react-dom@npm:18.2.18": + version: 18.2.18 + resolution: "@types/react-dom@npm:18.2.18" dependencies: "@types/react": "*" - checksum: 7a4e704ed4be6e0c3ccd8a22ff69386fe548304bf4db090513f42e059ff4c65f7a427790320051524d6578a2e4c9667bb7a80a4c989b72361c019fbe851d9385 + checksum: 8e3da404c980e2b2a76da3852f812ea6d8b9d0e7f5923fbaf3bfbbbfa1d59116ff91c129de8f68e9b7668a67ae34484fe9df74d5a7518cf8591ec07a0c4dad57 languageName: node linkType: hard @@ -7965,15 +7949,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:6.13.1": - version: 6.13.1 - resolution: "@typescript-eslint/eslint-plugin@npm:6.13.1" +"@typescript-eslint/eslint-plugin@npm:6.18.1": + version: 6.18.1 + resolution: "@typescript-eslint/eslint-plugin@npm:6.18.1" dependencies: "@eslint-community/regexpp": ^4.5.1 - "@typescript-eslint/scope-manager": 6.13.1 - "@typescript-eslint/type-utils": 6.13.1 - "@typescript-eslint/utils": 6.13.1 - "@typescript-eslint/visitor-keys": 6.13.1 + "@typescript-eslint/scope-manager": 6.18.1 + "@typescript-eslint/type-utils": 6.18.1 + "@typescript-eslint/utils": 6.18.1 + "@typescript-eslint/visitor-keys": 6.18.1 debug: ^4.3.4 graphemer: ^1.4.0 ignore: ^5.2.4 @@ -7986,25 +7970,25 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 568093d76c200a8502047d74f29300110a59b9f2a5cbf995a6cbe419c803a7ec22220e9592a884401d2dde72c79346b4cc0ee393e7b422924ad4a8a2040af3b0 + checksum: 933ede339bfac8377f94b211253bce40ace272a01466c290b38e681ec4752128ce63f827bbe6cc70cc0383d01655c8a22b25c640841fe90dfa4e57f73baaf2a9 languageName: node linkType: hard -"@typescript-eslint/parser@npm:6.13.1": - version: 6.13.1 - resolution: "@typescript-eslint/parser@npm:6.13.1" +"@typescript-eslint/parser@npm:6.18.1": + version: 6.18.1 + resolution: "@typescript-eslint/parser@npm:6.18.1" dependencies: - "@typescript-eslint/scope-manager": 6.13.1 - "@typescript-eslint/types": 6.13.1 - "@typescript-eslint/typescript-estree": 6.13.1 - "@typescript-eslint/visitor-keys": 6.13.1 + "@typescript-eslint/scope-manager": 6.18.1 + "@typescript-eslint/types": 6.18.1 + "@typescript-eslint/typescript-estree": 6.18.1 + "@typescript-eslint/visitor-keys": 6.18.1 debug: ^4.3.4 peerDependencies: eslint: ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 58b7fef6f2d02c8f4737f9908a8d335a20bee20dba648233a69f28e7b39237791d2b9fbb818e628dcc053ddf16507b161ace7f1139e093d72365f1270c426de3 + checksum: f123310976a73d9f08470dbad917c9e7b038e9e1362924a225a29d35fac1a2726d447952ca77b914d47f50791d235bb66f5171c7a4a0536e9c170fb20e73a2e4 languageName: node linkType: hard @@ -8018,22 +8002,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:6.13.1": - version: 6.13.1 - resolution: "@typescript-eslint/scope-manager@npm:6.13.1" +"@typescript-eslint/scope-manager@npm:6.18.1": + version: 6.18.1 + resolution: "@typescript-eslint/scope-manager@npm:6.18.1" dependencies: - "@typescript-eslint/types": 6.13.1 - "@typescript-eslint/visitor-keys": 6.13.1 - checksum: 109a213f82719e10f8c6a0168f2e105dc1369c7e0c075c1f30af137030fc866a3a585a77ff78a9a3538afc213061c8aedbb4462a91f26cbd90eefbab8b89ea10 + "@typescript-eslint/types": 6.18.1 + "@typescript-eslint/visitor-keys": 6.18.1 + checksum: d6708f9f2658ab68f9f4628b93c4131fb82c362383b4d5d671491082ff610258f2fc9e293739618dc76ed6d2c5909f000a54b9b905e58a5172e6e2f731666245 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:6.13.1": - version: 6.13.1 - resolution: "@typescript-eslint/type-utils@npm:6.13.1" +"@typescript-eslint/type-utils@npm:6.18.1": + version: 6.18.1 + resolution: "@typescript-eslint/type-utils@npm:6.18.1" dependencies: - "@typescript-eslint/typescript-estree": 6.13.1 - "@typescript-eslint/utils": 6.13.1 + "@typescript-eslint/typescript-estree": 6.18.1 + "@typescript-eslint/utils": 6.18.1 debug: ^4.3.4 ts-api-utils: ^1.0.1 peerDependencies: @@ -8041,7 +8025,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: e39d28dd2f3b47a26b4f6aa2c7a301bdd769ce9148d734be93441a813c3d1111eba1d655677355bba5519f3d4dbe93e4ff4e46830216b0302df0070bf7a80057 + checksum: 44d7e14460f8a22a0c5c58ff7004cb40061e722dfcec3ac4ee15d40dafe68c61e555a79e81af8ffa0ca845fb3caf3ed5376853b9a94e2f3c823ac5e8267230c8 languageName: node linkType: hard @@ -8052,10 +8036,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:6.13.1": - version: 6.13.1 - resolution: "@typescript-eslint/types@npm:6.13.1" - checksum: bb1d52f1646bab9acd3ec874567ffbaaaf7fe4a5f79845bdacbfea46d15698e58d45797da05b08c23f9496a17229b7f2c1363d000fd89ce4e79874fd57ba1d4a +"@typescript-eslint/types@npm:6.18.1": + version: 6.18.1 + resolution: "@typescript-eslint/types@npm:6.18.1" + checksum: f1713785c4dd49e6aae4186042679d205312a1c6cbfcdad133abf5c61f71c115e04c6643aa6a8aacd732e6b64030d71bbc92762164b7f231d98fc2e31c3f8ed8 languageName: node linkType: hard @@ -8077,38 +8061,39 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:6.13.1": - version: 6.13.1 - resolution: "@typescript-eslint/typescript-estree@npm:6.13.1" +"@typescript-eslint/typescript-estree@npm:6.18.1": + version: 6.18.1 + resolution: "@typescript-eslint/typescript-estree@npm:6.18.1" dependencies: - "@typescript-eslint/types": 6.13.1 - "@typescript-eslint/visitor-keys": 6.13.1 + "@typescript-eslint/types": 6.18.1 + "@typescript-eslint/visitor-keys": 6.18.1 debug: ^4.3.4 globby: ^11.1.0 is-glob: ^4.0.3 + minimatch: 9.0.3 semver: ^7.5.4 ts-api-utils: ^1.0.1 peerDependenciesMeta: typescript: optional: true - checksum: 09aa0f5cbd60e84df4f58f3d479be352549600b24dbefe75c686ea89252526c52c1c06ce1ae56c0405dd7337002e741c2ba02b71fb1caa3b94a740a70fcc8699 + checksum: fc5fb8abea9a6c3b774f62989b9a463569d141c32f6f2febef11d4161acaff946b204226234077b1126294fcf86a83c5fc9227f34ea3ba4cc9d39ca843dfae97 languageName: node linkType: hard -"@typescript-eslint/utils@npm:6.13.1": - version: 6.13.1 - resolution: "@typescript-eslint/utils@npm:6.13.1" +"@typescript-eslint/utils@npm:6.18.1": + version: 6.18.1 + resolution: "@typescript-eslint/utils@npm:6.18.1" dependencies: "@eslint-community/eslint-utils": ^4.4.0 "@types/json-schema": ^7.0.12 "@types/semver": ^7.5.0 - "@typescript-eslint/scope-manager": 6.13.1 - "@typescript-eslint/types": 6.13.1 - "@typescript-eslint/typescript-estree": 6.13.1 + "@typescript-eslint/scope-manager": 6.18.1 + "@typescript-eslint/types": 6.18.1 + "@typescript-eslint/typescript-estree": 6.18.1 semver: ^7.5.4 peerDependencies: eslint: ^7.0.0 || ^8.0.0 - checksum: 14f64840869c8755af4d287cfc74abc424dc139559e87ca1a8b0e850f4fa56311d99dfb61a43dd4433eae5914be12b4b3390e55de1f236dce6701830d17e31c9 + checksum: b7265b0cae099feb98e233dd518b54408fde01b9703535c9e9b84c24e9af6fff0fd9a61f0f7d7b24fb738151ad25a7f57210e83a5a2700cac38ee627f5b856d4 languageName: node linkType: hard @@ -8140,13 +8125,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:6.13.1": - version: 6.13.1 - resolution: "@typescript-eslint/visitor-keys@npm:6.13.1" +"@typescript-eslint/visitor-keys@npm:6.18.1": + version: 6.18.1 + resolution: "@typescript-eslint/visitor-keys@npm:6.18.1" dependencies: - "@typescript-eslint/types": 6.13.1 + "@typescript-eslint/types": 6.18.1 eslint-visitor-keys: ^3.4.1 - checksum: d15d362203a2fe995ea62a59d5b44c15c8fb1fb30ff59dd1542a980f75b3b62035303dfb781d83709921613f6ac8cc5bf57b70f6e20d820aec8b7911f07152e9 + checksum: 4befc450fd459e9dc368c3da7066a4948946e8b24383bf0fbaacd059cbe69ff0f71cac4f6d5d1f99a523c1fb20d39bef907e522d2c8e8315a8ce4ce678a58540 languageName: node linkType: hard @@ -8863,7 +8848,7 @@ __metadata: "@apollo/federation-subgraph-compatibility": 2.1.0 "@apollo/server": ^4.7.0 "@graphql-tools/wrap": ^10.0.0 - "@neo4j/graphql": ^4.4.4 + "@neo4j/graphql": ^4.4.5 fork-ts-checker-webpack-plugin: 9.0.2 graphql: 16.8.1 graphql-tag: ^2.12.6 @@ -10501,7 +10486,14 @@ __metadata: languageName: node linkType: hard -"classnames@npm:2.3.2, classnames@npm:^2.2.6, classnames@npm:^2.3.1": +"classnames@npm:2.5.1": + version: 2.5.1 + resolution: "classnames@npm:2.5.1" + checksum: da424a8a6f3a96a2e87d01a432ba19315503294ac7e025f9fece656db6b6a0f7b5003bb1fbb51cbb0d9624d964f1b9bb35a51c73af9b2434c7b292c42231c1e5 + languageName: node + linkType: hard + +"classnames@npm:^2.2.6, classnames@npm:^2.3.1": version: 2.3.2 resolution: "classnames@npm:2.3.2" checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e @@ -10558,6 +10550,15 @@ __metadata: languageName: node linkType: hard +"cli-progress@npm:^3.12.0": + version: 3.12.0 + resolution: "cli-progress@npm:3.12.0" + dependencies: + string-width: ^4.2.3 + checksum: e8390dc3cdf3c72ecfda0a1e8997bfed63a0d837f97366bbce0ca2ff1b452da386caed007b389f0fe972625037b6c8e7ab087c69d6184cc4dfc8595c4c1d3e6e + languageName: node + linkType: hard + "cli-spinners@npm:^2.5.0": version: 2.9.1 resolution: "cli-spinners@npm:2.9.1" @@ -11204,13 +11205,13 @@ __metadata: languageName: node linkType: hard -"cookies@npm:~0.8.0": - version: 0.8.0 - resolution: "cookies@npm:0.8.0" +"cookies@npm:~0.9.0": + version: 0.9.0 + resolution: "cookies@npm:0.9.0" dependencies: depd: ~2.0.0 keygrip: ~1.1.0 - checksum: 806055a44f128705265b1bc6a853058da18bf80dea3654ad99be20985b1fa1b14f86c1eef73644aab8071241f8a78acd57202b54c4c5c70769fc694fbb9c4edc + checksum: e4db8b65edc85bb9640ddca88abae319cbde21af56d83b9b0a4346d41450166bc072f19d0eca00cc805be1c61979a6e80f7f38f5af517ffe2549a44ff5812953 languageName: node linkType: hard @@ -11283,7 +11284,7 @@ __metadata: languageName: node linkType: hard -"cosmiconfig@npm:^8.0.0, cosmiconfig@npm:^8.2.0": +"cosmiconfig@npm:^8.0.0, cosmiconfig@npm:^8.2.0, cosmiconfig@npm:^8.3.5": version: 8.3.6 resolution: "cosmiconfig@npm:8.3.6" dependencies: @@ -11489,21 +11490,21 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:6.8.1": - version: 6.8.1 - resolution: "css-loader@npm:6.8.1" +"css-loader@npm:6.9.0": + version: 6.9.0 + resolution: "css-loader@npm:6.9.0" dependencies: icss-utils: ^5.1.0 - postcss: ^8.4.21 + postcss: ^8.4.31 postcss-modules-extract-imports: ^3.0.0 postcss-modules-local-by-default: ^4.0.3 - postcss-modules-scope: ^3.0.0 + postcss-modules-scope: ^3.1.0 postcss-modules-values: ^4.0.0 postcss-value-parser: ^4.2.0 - semver: ^7.3.8 + semver: ^7.5.4 peerDependencies: webpack: ^5.0.0 - checksum: 7c1784247bdbe76dc5c55fb1ac84f1d4177a74c47259942c9cfdb7a8e6baef11967a0bc85ac285f26bd26d5059decb848af8154a03fdb4f4894f41212f45eef3 + checksum: 71f20ee5eb5a4a9373ab41a5c17df411cb4f6f2de037297a2b0c2150681578f4979f319f4307a61e23c231dd6546e657ae95cba5a0698ad13ca43f91d4d2a0bc languageName: node linkType: hard @@ -12837,7 +12838,7 @@ __metadata: languageName: node linkType: hard -"entities@npm:^3.0.1, entities@npm:~3.0.1": +"entities@npm:^3.0.1": version: 3.0.1 resolution: "entities@npm:3.0.1" checksum: aaf7f12033f0939be91f5161593f853f2da55866db55ccbf72f45430b8977e2b79dbd58c53d0fdd2d00bd7d313b75b0968d09f038df88e308aa97e39f9456572 @@ -13254,9 +13255,9 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-import@npm:2.29.0": - version: 2.29.0 - resolution: "eslint-plugin-import@npm:2.29.0" +"eslint-plugin-import@npm:2.29.1": + version: 2.29.1 + resolution: "eslint-plugin-import@npm:2.29.1" dependencies: array-includes: ^3.1.7 array.prototype.findlastindex: ^1.2.3 @@ -13274,16 +13275,16 @@ __metadata: object.groupby: ^1.0.1 object.values: ^1.1.7 semver: ^6.3.1 - tsconfig-paths: ^3.14.2 + tsconfig-paths: ^3.15.0 peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - checksum: 19ee541fb95eb7a796f3daebe42387b8d8262bbbcc4fd8a6e92f63a12035f3d2c6cb8bc0b6a70864fa14b1b50ed6b8e6eed5833e625e16cb6bb98b665beff269 + checksum: e65159aef808136d26d029b71c8c6e4cb5c628e65e5de77f1eb4c13a379315ae55c9c3afa847f43f4ff9df7e54515c77ffc6489c6a6f81f7dd7359267577468c languageName: node linkType: hard -"eslint-plugin-jest@npm:27.6.0": - version: 27.6.0 - resolution: "eslint-plugin-jest@npm:27.6.0" +"eslint-plugin-jest@npm:27.6.2": + version: 27.6.2 + resolution: "eslint-plugin-jest@npm:27.6.2" dependencies: "@typescript-eslint/utils": ^5.10.0 peerDependencies: @@ -13295,7 +13296,7 @@ __metadata: optional: true jest: optional: true - checksum: 4c42641f9bf2d597761637028083e20b9f81762308e98baae40eb805d3e81ff8d837f06f4f0c1a2fd249e2be2fb24d33b7aafeaa8942de805c2b8d7c3b6fc4e4 + checksum: 9c2a950d0e7e76dd1b55016800088ab86ce590df24d0abf8573b09c547969daa395157de55408307824deae1cfde136cbaffb85932f28eceec0689f68cbdd3bf languageName: node linkType: hard @@ -13387,14 +13388,14 @@ __metadata: languageName: node linkType: hard -"eslint@npm:8.55.0": - version: 8.55.0 - resolution: "eslint@npm:8.55.0" +"eslint@npm:8.56.0": + version: 8.56.0 + resolution: "eslint@npm:8.56.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.6.1 "@eslint/eslintrc": ^2.1.4 - "@eslint/js": 8.55.0 + "@eslint/js": 8.56.0 "@humanwhocodes/config-array": ^0.11.13 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 @@ -13431,7 +13432,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 83f82a604559dc1faae79d28fdf3dfc9e592ca221052e2ea516e1b379b37e77e4597705a16880e2f5ece4f79087c1dd13fd7f6e9746f794a401175519db18b41 + checksum: 883436d1e809b4a25d9eb03d42f584b84c408dbac28b0019f6ea07b5177940bf3cca86208f749a6a1e0039b63e085ee47aca1236c30721e91f0deef5cc5a5136 languageName: node linkType: hard @@ -15028,12 +15029,12 @@ __metadata: languageName: node linkType: hard -"graphql-ws@npm:5.14.2": - version: 5.14.2 - resolution: "graphql-ws@npm:5.14.2" +"graphql-ws@npm:5.14.3": + version: 5.14.3 + resolution: "graphql-ws@npm:5.14.3" peerDependencies: graphql: ">=0.11 <=16" - checksum: ee9affa2478b9d262405986f07616267b4db10ae45cf32fffb551572fb5bf5e1e3aa6652375511b3ff640d382c74c1327ce75ff1ee2fa8b964b3ef3d55d97f75 + checksum: c5bfdeb6d06f528e2222e71bf830b2f4f3e5b95419453d3b650cef9fe012e0126f121e4858d950edf3db1fb209a056b592643751624d1bc1fc71ecbe546d53d5 languageName: node linkType: hard @@ -15471,9 +15472,9 @@ __metadata: languageName: node linkType: hard -"html-webpack-plugin@npm:5.5.3": - version: 5.5.3 - resolution: "html-webpack-plugin@npm:5.5.3" +"html-webpack-plugin@npm:5.6.0": + version: 5.6.0 + resolution: "html-webpack-plugin@npm:5.6.0" dependencies: "@types/html-minifier-terser": ^6.0.0 html-minifier-terser: ^6.0.2 @@ -15481,8 +15482,14 @@ __metadata: pretty-error: ^4.0.0 tapable: ^2.0.0 peerDependencies: + "@rspack/core": 0.x || 1.x webpack: ^5.20.0 - checksum: ccf685195739c372ad641bbd0c9100a847904f34eedc7aff3ece7856cd6c78fd3746d2d615af1bb71e5727993fe711b89e9b744f033ed3fde646540bf5d5e954 + peerDependenciesMeta: + "@rspack/core": + optional: true + webpack: + optional: true + checksum: 32a6e41da538e798fd0be476637d7611a5e8a98a3508f031996e9eb27804dcdc282cb01f847cf5d066f21b49cfb8e21627fcf977ffd0c9bea81cf80e5a65070d languageName: node linkType: hard @@ -17414,7 +17421,7 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^1.18.2, jiti@npm:^1.19.1": +"jiti@npm:^1.19.1": version: 1.20.0 resolution: "jiti@npm:1.20.0" bin: @@ -17423,6 +17430,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^1.20.0": + version: 1.21.0 + resolution: "jiti@npm:1.21.0" + bin: + jiti: bin/jiti.js + checksum: a7bd5d63921c170eaec91eecd686388181c7828e1fa0657ab374b9372bfc1f383cf4b039e6b272383d5cb25607509880af814a39abdff967322459cca41f2961 + languageName: node + linkType: hard + "jose@npm:^4.14.6": version: 4.15.2 resolution: "jose@npm:4.15.2" @@ -17856,15 +17872,15 @@ __metadata: languageName: node linkType: hard -"koa@npm:2.14.2": - version: 2.14.2 - resolution: "koa@npm:2.14.2" +"koa@npm:2.15.0": + version: 2.15.0 + resolution: "koa@npm:2.15.0" dependencies: accepts: ^1.3.5 cache-content-type: ^1.0.0 content-disposition: ~0.5.2 content-type: ^1.0.4 - cookies: ~0.8.0 + cookies: ~0.9.0 debug: ^4.3.2 delegates: ^1.0.0 depd: ^2.0.0 @@ -17883,7 +17899,7 @@ __metadata: statuses: ^1.5.0 type-is: ^1.6.16 vary: ^1.1.2 - checksum: 17fe3b8f5e0b4759004a942cc6ba2a9507299943a697dff9766b85f41f45caed4077ca2645ac9ad254d3359fffedfc4c9ebdd7a70493e5df8cdfac159a8ee835 + checksum: a97741f89f328f25ae94d82d0ee608377d89e086c73f2d868023e6050dea682ef93e0a5c80097f3aaad28121853aea50a7fb3c0c12ecc45798da2fd1255f580b languageName: node linkType: hard @@ -17982,12 +17998,12 @@ __metadata: languageName: node linkType: hard -"libnpmsearch@npm:7.0.0": - version: 7.0.0 - resolution: "libnpmsearch@npm:7.0.0" +"libnpmsearch@npm:7.0.1": + version: 7.0.1 + resolution: "libnpmsearch@npm:7.0.1" dependencies: npm-registry-fetch: ^16.0.0 - checksum: 3b10c35b6feb84622d1ca0009e02ae577b5648f10aa28c7c989a4db64ec3bc4f2f866a1c4c07002ae319ac40bda1608fc87bbcfad248f8104946120d58b45e2b + checksum: 97a12306a4f4c8c15eef574b2cf95f3c556c751a7cdf6f7fb9a62bacd79ff1f70300e5bde0c652b4d5e67ae6516ca41b9c43783cafa2c1c4dad6818463cd58a2 languageName: node linkType: hard @@ -18002,83 +18018,83 @@ __metadata: languageName: node linkType: hard -"lightningcss-darwin-arm64@npm:1.22.0": - version: 1.22.0 - resolution: "lightningcss-darwin-arm64@npm:1.22.0" +"lightningcss-darwin-arm64@npm:1.22.1": + version: 1.22.1 + resolution: "lightningcss-darwin-arm64@npm:1.22.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"lightningcss-darwin-x64@npm:1.22.0": - version: 1.22.0 - resolution: "lightningcss-darwin-x64@npm:1.22.0" +"lightningcss-darwin-x64@npm:1.22.1": + version: 1.22.1 + resolution: "lightningcss-darwin-x64@npm:1.22.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"lightningcss-freebsd-x64@npm:1.22.0": - version: 1.22.0 - resolution: "lightningcss-freebsd-x64@npm:1.22.0" +"lightningcss-freebsd-x64@npm:1.22.1": + version: 1.22.1 + resolution: "lightningcss-freebsd-x64@npm:1.22.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"lightningcss-linux-arm-gnueabihf@npm:1.22.0": - version: 1.22.0 - resolution: "lightningcss-linux-arm-gnueabihf@npm:1.22.0" +"lightningcss-linux-arm-gnueabihf@npm:1.22.1": + version: 1.22.1 + resolution: "lightningcss-linux-arm-gnueabihf@npm:1.22.1" conditions: os=linux & cpu=arm languageName: node linkType: hard -"lightningcss-linux-arm64-gnu@npm:1.22.0": - version: 1.22.0 - resolution: "lightningcss-linux-arm64-gnu@npm:1.22.0" +"lightningcss-linux-arm64-gnu@npm:1.22.1": + version: 1.22.1 + resolution: "lightningcss-linux-arm64-gnu@npm:1.22.1" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"lightningcss-linux-arm64-musl@npm:1.22.0": - version: 1.22.0 - resolution: "lightningcss-linux-arm64-musl@npm:1.22.0" +"lightningcss-linux-arm64-musl@npm:1.22.1": + version: 1.22.1 + resolution: "lightningcss-linux-arm64-musl@npm:1.22.1" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"lightningcss-linux-x64-gnu@npm:1.22.0": - version: 1.22.0 - resolution: "lightningcss-linux-x64-gnu@npm:1.22.0" +"lightningcss-linux-x64-gnu@npm:1.22.1": + version: 1.22.1 + resolution: "lightningcss-linux-x64-gnu@npm:1.22.1" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"lightningcss-linux-x64-musl@npm:1.22.0": - version: 1.22.0 - resolution: "lightningcss-linux-x64-musl@npm:1.22.0" +"lightningcss-linux-x64-musl@npm:1.22.1": + version: 1.22.1 + resolution: "lightningcss-linux-x64-musl@npm:1.22.1" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"lightningcss-win32-x64-msvc@npm:1.22.0": - version: 1.22.0 - resolution: "lightningcss-win32-x64-msvc@npm:1.22.0" +"lightningcss-win32-x64-msvc@npm:1.22.1": + version: 1.22.1 + resolution: "lightningcss-win32-x64-msvc@npm:1.22.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"lightningcss@npm:^1.16.1": - version: 1.22.0 - resolution: "lightningcss@npm:1.22.0" +"lightningcss@npm:^1.22.1": + version: 1.22.1 + resolution: "lightningcss@npm:1.22.1" dependencies: detect-libc: ^1.0.3 - lightningcss-darwin-arm64: 1.22.0 - lightningcss-darwin-x64: 1.22.0 - lightningcss-freebsd-x64: 1.22.0 - lightningcss-linux-arm-gnueabihf: 1.22.0 - lightningcss-linux-arm64-gnu: 1.22.0 - lightningcss-linux-arm64-musl: 1.22.0 - lightningcss-linux-x64-gnu: 1.22.0 - lightningcss-linux-x64-musl: 1.22.0 - lightningcss-win32-x64-msvc: 1.22.0 + lightningcss-darwin-arm64: 1.22.1 + lightningcss-darwin-x64: 1.22.1 + lightningcss-freebsd-x64: 1.22.1 + lightningcss-linux-arm-gnueabihf: 1.22.1 + lightningcss-linux-arm64-gnu: 1.22.1 + lightningcss-linux-arm64-musl: 1.22.1 + lightningcss-linux-x64-gnu: 1.22.1 + lightningcss-linux-x64-musl: 1.22.1 + lightningcss-win32-x64-msvc: 1.22.1 dependenciesMeta: lightningcss-darwin-arm64: optional: true @@ -18098,7 +18114,7 @@ __metadata: optional: true lightningcss-win32-x64-msvc: optional: true - checksum: 6b9a04846243a2161ac12ee098f9c2143a1a06fb683228ef0433473257751a709b0bafa195efa8d3d8f1556ca60c54f5434caeb172874a8daced552dedcbed93 + checksum: 75319e14cae842f92d2d3fbf3c7616ef427298fc3bd010bc644eb67c21af93debc2dff5dcf67b6dcf0eab0ca6c073bc670805bba1977cf3423d0da766e15caf3 languageName: node linkType: hard @@ -18139,15 +18155,6 @@ __metadata: languageName: node linkType: hard -"linkify-it@npm:^4.0.1": - version: 4.0.1 - resolution: "linkify-it@npm:4.0.1" - dependencies: - uc.micro: ^1.0.1 - checksum: 3e0a29921269c14eb7ac6f5db2da68d4854ea9acca6e9014a323f75f2dd39b197ffab57c1fbd6a906ceb021aad3ee6d7ba7d0181236dd9630ffc452b392f7f71 - languageName: node - linkType: hard - "lint-staged@npm:15.2.0": version: 15.2.0 resolution: "lint-staged@npm:15.2.0" @@ -18820,21 +18827,6 @@ __metadata: languageName: node linkType: hard -"markdown-it@npm:13.0.2": - version: 13.0.2 - resolution: "markdown-it@npm:13.0.2" - dependencies: - argparse: ^2.0.1 - entities: ~3.0.1 - linkify-it: ^4.0.1 - mdurl: ^1.0.1 - uc.micro: ^1.0.5 - bin: - markdown-it: bin/markdown-it.js - checksum: bb4bf2cb3e5d77a7f3dc9cf48e17d050fbcd26d37992204eaa5812220752858fe9debe439b2ae1de06e749a3bba537c0baf6ce7510307cf7163a70f04fafe672 - languageName: node - linkType: hard - "markdown-it@npm:^12.2.0": version: 12.3.2 resolution: "markdown-it@npm:12.3.2" @@ -19158,6 +19150,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:9.0.3, minimatch@npm:^9.0.1": + version: 9.0.3 + resolution: "minimatch@npm:9.0.3" + dependencies: + brace-expansion: ^2.0.1 + checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 + languageName: node + linkType: hard + "minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" @@ -19176,15 +19177,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.1": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" - dependencies: - brace-expansion: ^2.0.1 - checksum: 253487976bf485b612f16bf57463520a14f512662e592e95c571afdab1442a6a6864b6c88f248ce6fc4ff0b6de04ac7aa6c8bb51e868e99d1d65eb0658a708b5 - languageName: node - linkType: hard - "minimist-options@npm:4.1.0, minimist-options@npm:^4.0.2": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -19806,7 +19798,7 @@ __metadata: graphql: ^16.6.0 graphql-ws: ^5.11.2 neo4j-driver: ^5.8.0 - parcel: 2.10.3 + parcel: 2.11.0 rimraf: ^5.0.0 urql: ^4.0.0 wonka: ^6.1.1 @@ -19825,14 +19817,14 @@ __metadata: languageName: node linkType: hard -"neo4j-driver-bolt-connection@npm:5.15.0": - version: 5.15.0 - resolution: "neo4j-driver-bolt-connection@npm:5.15.0" +"neo4j-driver-bolt-connection@npm:5.16.0": + version: 5.16.0 + resolution: "neo4j-driver-bolt-connection@npm:5.16.0" dependencies: buffer: ^6.0.3 - neo4j-driver-core: 5.15.0 + neo4j-driver-core: 5.16.0 string_decoder: ^1.3.0 - checksum: 5e36d6eab0130df8d15e71ad85ca36697292e32ea4195ec7f33a547675942f3148a30c5aa99fc93e830d4b3fb771d50d05f98a12f1134bd7f06d2c09a3ca8d36 + checksum: ddf5337fba3bd31b5909c1ab351d57df3b51d971786ba262d53ba38e89c4204d78cf7401527814e0ea6541dea26d21b54627e17fed4b9e78e2ee31f46b3deaf3 languageName: node linkType: hard @@ -19843,21 +19835,21 @@ __metadata: languageName: node linkType: hard -"neo4j-driver-core@npm:5.15.0": - version: 5.15.0 - resolution: "neo4j-driver-core@npm:5.15.0" - checksum: c13ba8cf0a68ee5de7d3855aa1e5b8e724c8f735ab5514e8dd8cfaee71d808faf38b9511666d223005a10340a1929a7d90074c7596e7f25daa47bb77fcbd0a96 +"neo4j-driver-core@npm:5.16.0": + version: 5.16.0 + resolution: "neo4j-driver-core@npm:5.16.0" + checksum: 58c33ea0121bdbd4d541c1967998fae1b07a092a8b83438c78567dd008f79ab3401a97824c9ccb721bb25c9ff65485521bb0a1b33fa75e3464a5b6324e65d593 languageName: node linkType: hard -"neo4j-driver@npm:5.15.0": - version: 5.15.0 - resolution: "neo4j-driver@npm:5.15.0" +"neo4j-driver@npm:5.16.0": + version: 5.16.0 + resolution: "neo4j-driver@npm:5.16.0" dependencies: - neo4j-driver-bolt-connection: 5.15.0 - neo4j-driver-core: 5.15.0 + neo4j-driver-bolt-connection: 5.16.0 + neo4j-driver-core: 5.16.0 rxjs: ^7.8.1 - checksum: 9e80d00719c1ec24bddfd9f221690db992e4102cb511f7809ae6f77f3fbc555b6420f205b24a6132cd91784356323c69eefc3731ee40c919faaaf791b03eafbb + checksum: ac6beea48043a379149132514fbabcd8ce2604c2e8237fdd60729244834a9ce45f5379aa6ca4fb93f008beaed9c3c4414e7087cd5b9d1e9c3397715fb74aa9e3 languageName: node linkType: hard @@ -19879,17 +19871,17 @@ __metadata: "@changesets/changelog-github": 0.5.0 "@changesets/cli": 2.27.1 "@tsconfig/node16": 1.0.4 - "@typescript-eslint/eslint-plugin": 6.13.1 - "@typescript-eslint/parser": 6.13.1 + "@typescript-eslint/eslint-plugin": 6.18.1 + "@typescript-eslint/parser": 6.18.1 concurrently: 8.2.2 dotenv: 16.3.1 - eslint: 8.55.0 + eslint: 8.56.0 eslint-config-prettier: 9.1.0 eslint-formatter-summary: 1.1.0 eslint-import-resolver-typescript: 3.6.1 eslint-plugin-eslint-comments: 3.2.0 - eslint-plugin-import: 2.29.0 - eslint-plugin-jest: 27.6.0 + eslint-plugin-import: 2.29.1 + eslint-plugin-jest: 27.6.2 eslint-plugin-jsx-a11y: 6.8.0 eslint-plugin-react: 7.33.2 eslint-plugin-simple-import-sort: 10.0.0 @@ -19897,7 +19889,7 @@ __metadata: husky: 8.0.3 jest: 29.7.0 lint-staged: 15.2.0 - neo4j-driver: 5.15.0 + neo4j-driver: 5.16.0 npm-run-all: 4.1.5 prettier: 2.8.8 set-tz: 0.2.0 @@ -20984,27 +20976,27 @@ __metadata: languageName: node linkType: hard -"parcel@npm:2.10.3": - version: 2.10.3 - resolution: "parcel@npm:2.10.3" +"parcel@npm:2.11.0": + version: 2.11.0 + resolution: "parcel@npm:2.11.0" dependencies: - "@parcel/config-default": 2.10.3 - "@parcel/core": 2.10.3 - "@parcel/diagnostic": 2.10.3 - "@parcel/events": 2.10.3 - "@parcel/fs": 2.10.3 - "@parcel/logger": 2.10.3 - "@parcel/package-manager": 2.10.3 - "@parcel/reporter-cli": 2.10.3 - "@parcel/reporter-dev-server": 2.10.3 - "@parcel/reporter-tracer": 2.10.3 - "@parcel/utils": 2.10.3 + "@parcel/config-default": 2.11.0 + "@parcel/core": 2.11.0 + "@parcel/diagnostic": 2.11.0 + "@parcel/events": 2.11.0 + "@parcel/fs": 2.11.0 + "@parcel/logger": 2.11.0 + "@parcel/package-manager": 2.11.0 + "@parcel/reporter-cli": 2.11.0 + "@parcel/reporter-dev-server": 2.11.0 + "@parcel/reporter-tracer": 2.11.0 + "@parcel/utils": 2.11.0 chalk: ^4.1.0 commander: ^7.0.0 get-port: ^4.2.0 bin: parcel: lib/bin.js - checksum: 3a277e8e85064227b2d3335520b272023b887df6ef4a8b56a145e00bec7c1fab581081bb0d9bdad026dc4fee64e622ae6386da2d34cc716e7a83ad97ac970100 + checksum: 1990c725049161755c3556effc8d940fb6d05edfd2f2a74bb507d0d9bdd17d2e6d73ac45d1ded9ff31d94549a364cd109c2827de6a0a956e672aa0999f283879 languageName: node linkType: hard @@ -21601,17 +21593,17 @@ __metadata: languageName: node linkType: hard -"postcss-loader@npm:7.3.3": - version: 7.3.3 - resolution: "postcss-loader@npm:7.3.3" +"postcss-loader@npm:7.3.4": + version: 7.3.4 + resolution: "postcss-loader@npm:7.3.4" dependencies: - cosmiconfig: ^8.2.0 - jiti: ^1.18.2 - semver: ^7.3.8 + cosmiconfig: ^8.3.5 + jiti: ^1.20.0 + semver: ^7.5.4 peerDependencies: postcss: ^7.0.0 || ^8.0.1 webpack: ^5.0.0 - checksum: c724044d6ae56334535c26bb4efc9c151431d44d60bc8300157c760747281a242757d8dab32db72738434531175b38a408cb0b270bb96207c07584dcfcd899ff + checksum: f109eb266580eb296441a1ae057f93629b9b79ad962bdd3fc134417180431606a5419b6f5848c31e6d92c818e71fe96e4335a85cc5332c2f7b14e2869951e5b3 languageName: node linkType: hard @@ -21637,14 +21629,14 @@ __metadata: languageName: node linkType: hard -"postcss-modules-scope@npm:^3.0.0": - version: 3.0.0 - resolution: "postcss-modules-scope@npm:3.0.0" +"postcss-modules-scope@npm:^3.1.0": + version: 3.1.0 + resolution: "postcss-modules-scope@npm:3.1.0" dependencies: postcss-selector-parser: ^6.0.4 peerDependencies: postcss: ^8.1.0 - checksum: 330b9398dbd44c992c92b0dc612c0626135e2cc840fee41841eb61247a6cfed95af2bd6f67ead9dd9d0bb41f5b0367129d93c6e434fa3e9c58ade391d9a5a138 + checksum: 919d02e2e31956fa3dae2036d4f3259c9b8c5361bd58ee55867edededbee03507df88e98f418b5e553e47f3888daba9ea9ef0b18a82c41cf96cdb74df15322c7 languageName: node linkType: hard @@ -21687,18 +21679,18 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.4.32": - version: 8.4.32 - resolution: "postcss@npm:8.4.32" +"postcss@npm:8.4.33, postcss@npm:^8.4.31": + version: 8.4.33 + resolution: "postcss@npm:8.4.33" dependencies: nanoid: ^3.3.7 picocolors: ^1.0.0 source-map-js: ^1.0.2 - checksum: 220d9d0bf5d65be7ed31006c523bfb11619461d296245c1231831f90150aeb4a31eab9983ac9c5c89759a3ca8b60b3e0d098574964e1691673c3ce5c494305ae + checksum: 6f98b2af4b76632a3de20c4f47bf0e984a1ce1a531cf11adcb0b1d63a6cbda0aae4165e578b66c32ca4879038e3eaad386a6be725a8fb4429c78e3c1ab858fe9 languageName: node linkType: hard -"postcss@npm:^8.1.10, postcss@npm:^8.4.21, postcss@npm:^8.4.23": +"postcss@npm:^8.1.10, postcss@npm:^8.4.23": version: 8.4.30 resolution: "postcss@npm:8.4.30" dependencies: @@ -24602,12 +24594,12 @@ __metadata: languageName: node linkType: hard -"style-loader@npm:3.3.3": - version: 3.3.3 - resolution: "style-loader@npm:3.3.3" +"style-loader@npm:3.3.4": + version: 3.3.4 + resolution: "style-loader@npm:3.3.4" peerDependencies: webpack: ^5.0.0 - checksum: f59c953f56f6a935bd6a1dfa409f1128fed2b66b48ce4a7a75b85862a7156e5e90ab163878962762f528ec4d510903d828da645e143fbffd26f055dc1c094078 + checksum: caac3f2fe2c3c89e49b7a2a9329e1cfa515ecf5f36b9c4885f9b218019fda207a9029939b2c35821dec177a264a007e7c391ccdd3ff7401881ce6287b9c8f38b languageName: node linkType: hard @@ -24834,9 +24826,9 @@ __metadata: languageName: node linkType: hard -"tailwindcss@npm:3.3.5": - version: 3.3.5 - resolution: "tailwindcss@npm:3.3.5" +"tailwindcss@npm:3.4.1": + version: 3.4.1 + resolution: "tailwindcss@npm:3.4.1" dependencies: "@alloc/quick-lru": ^5.2.0 arg: ^5.0.2 @@ -24863,7 +24855,7 @@ __metadata: bin: tailwind: lib/cli.js tailwindcss: lib/cli.js - checksum: e04bb3bb7f9f17e9b6db0c7ace755ef0d6d05bff36ebeb9e5006e13c018ed5566f09db30a1a34380e38fa93ebbb4ae0e28fe726879d5e9ddd8c5b52bffd26f14 + checksum: ef5a587dd32bb4e91e1549ead6162f85f0b78d3e6ffd8b4e8eeb15585b7b886cb3af6ae9df5092ed8ccb7e590608d1b3eec79ca08c862b07cd9ff7e72f73104b languageName: node linkType: hard @@ -24905,7 +24897,29 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:5.3.9, terser-webpack-plugin@npm:^5.3.7": +"terser-webpack-plugin@npm:5.3.10": + version: 5.3.10 + resolution: "terser-webpack-plugin@npm:5.3.10" + dependencies: + "@jridgewell/trace-mapping": ^0.3.20 + jest-worker: ^27.4.5 + schema-utils: ^3.1.1 + serialize-javascript: ^6.0.1 + terser: ^5.26.0 + peerDependencies: + webpack: ^5.1.0 + peerDependenciesMeta: + "@swc/core": + optional: true + esbuild: + optional: true + uglify-js: + optional: true + checksum: bd6e7596cf815f3353e2a53e79cbdec959a1b0276f5e5d4e63e9d7c3c5bb5306df567729da287d1c7b39d79093e56863c569c42c6c24cc34c76aa313bd2cbcea + languageName: node + linkType: hard + +"terser-webpack-plugin@npm:^5.3.7": version: 5.3.9 resolution: "terser-webpack-plugin@npm:5.3.9" dependencies: @@ -24954,6 +24968,20 @@ __metadata: languageName: node linkType: hard +"terser@npm:^5.26.0": + version: 5.26.0 + resolution: "terser@npm:5.26.0" + dependencies: + "@jridgewell/source-map": ^0.3.3 + acorn: ^8.8.2 + commander: ^2.20.0 + source-map-support: ~0.5.20 + bin: + terser: bin/terser + checksum: 02a9bb896f04df828025af8f0eced36c315d25d310b6c2418e7dad2bed19ddeb34a9cea9b34e7c24789830fa51e1b6a9be26679980987a9c817a7e6d9cd4154b + languageName: node + linkType: hard + "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" @@ -25337,9 +25365,9 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:10.9.1": - version: 10.9.1 - resolution: "ts-node@npm:10.9.1" +"ts-node@npm:10.9.2": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2" dependencies: "@cspotcode/source-map-support": ^0.8.0 "@tsconfig/node10": ^1.0.7 @@ -25371,7 +25399,7 @@ __metadata: ts-node-script: dist/bin-script.js ts-node-transpile-only: dist/bin-transpile.js ts-script: dist/bin-script-deprecated.js - checksum: 090adff1302ab20bd3486e6b4799e90f97726ed39e02b39e566f8ab674fd5bd5f727f43615debbfc580d33c6d9d1c6b1b3ce7d8e3cca3e20530a145ffa232c35 + checksum: fde256c9073969e234526e2cfead42591b9a2aec5222bac154b0de2fa9e4ceb30efcd717ee8bc785a56f3a119bdd5aa27b333d9dbec94ed254bd26f8944c67ac languageName: node linkType: hard @@ -25386,15 +25414,15 @@ __metadata: languageName: node linkType: hard -"tsconfig-paths@npm:^3.14.2": - version: 3.14.2 - resolution: "tsconfig-paths@npm:3.14.2" +"tsconfig-paths@npm:^3.15.0": + version: 3.15.0 + resolution: "tsconfig-paths@npm:3.15.0" dependencies: "@types/json5": ^0.0.29 json5: ^1.0.2 minimist: ^1.2.6 strip-bom: ^3.0.0 - checksum: a6162eaa1aed680537f93621b82399c7856afd10ec299867b13a0675e981acac4e0ec00896860480efc59fc10fd0b16fdc928c0b885865b52be62cadac692447 + checksum: 59f35407a390d9482b320451f52a411a256a130ff0e7543d18c6f20afab29ac19fbe55c360a93d6476213cc335a4d76ce90f67df54c4e9037f7d240920832201 languageName: node linkType: hard @@ -26082,6 +26110,16 @@ __metadata: languageName: node linkType: hard +"usehooks-ts@npm:2.9.1": + version: 2.9.1 + resolution: "usehooks-ts@npm:2.9.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 36f1e4142ce23bc019b81d2e93aefd7f2c350abcf255598c21627114a69a2f2f116b35dc3a353375f09c6e4c9b704a04f104e3d10e98280545c097feca66c30a + languageName: node + linkType: hard + "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -26908,9 +26946,9 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.14.2, ws@npm:^8.0.0, ws@npm:^8.11.0, ws@npm:^8.13.0, ws@npm:^8.2.2, ws@npm:^8.5.0": - version: 8.14.2 - resolution: "ws@npm:8.14.2" +"ws@npm:8.16.0": + version: 8.16.0 + resolution: "ws@npm:8.16.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -26919,7 +26957,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 3ca0dad26e8cc6515ff392b622a1467430814c463b3368b0258e33696b1d4bed7510bc7030f7b72838b9fdeb8dbd8839cbf808367d6aae2e1d668ce741d4308b + checksum: feb3eecd2bae82fa8a8beef800290ce437d8b8063bdc69712725f21aef77c49cb2ff45c6e5e7fce622248f9c7abaee506bae0a9064067ffd6935460c7357321b languageName: node linkType: hard @@ -26938,6 +26976,21 @@ __metadata: languageName: node linkType: hard +"ws@npm:^8.0.0, ws@npm:^8.11.0, ws@npm:^8.13.0, ws@npm:^8.2.2, ws@npm:^8.5.0": + version: 8.14.2 + resolution: "ws@npm:8.14.2" + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ">=5.0.2" + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + checksum: 3ca0dad26e8cc6515ff392b622a1467430814c463b3368b0258e33696b1d4bed7510bc7030f7b72838b9fdeb8dbd8839cbf808367d6aae2e1d668ce741d4308b + languageName: node + linkType: hard + "ws@npm:~7.4.0": version: 7.4.6 resolution: "ws@npm:7.4.6"