diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml index b5b68eb1f62..5072dad78d0 100644 --- a/.github/workflows/changesets.yml +++ b/.github/workflows/changesets.yml @@ -24,13 +24,13 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: fetch-depth: 0 persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* @@ -78,7 +78,7 @@ jobs: steps: - name: Send Slack announcement of release if: matrix.package.name == '@neo4j/graphql' - uses: slackapi/slack-github-action@v1.24.0 + uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0 with: payload: '{"version":"${{ matrix.package.version }}"}' env: @@ -94,7 +94,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: ref: master fetch-depth: 0 @@ -122,7 +122,7 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: fetch-depth: 0 diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml index 3c297233e86..254377baf70 100644 --- a/.github/workflows/cla-check.yml +++ b/.github/workflows/cla-check.yml @@ -13,11 +13,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: repository: neo-technology/whitelist-check token: ${{ secrets.NEO4J_TEAM_GRAPHQL_PERSONAL_ACCESS_TOKEN }} - - uses: actions/setup-python@v4 + - uses: actions/setup-python@65d7f2d534ac1bc67fcd62888c5f4f3d2cb2b236 # v4 with: python-version: 3 - name: Install dependencies diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 9afa050f99c..b69c2ae2dfd 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -70,7 +70,7 @@ jobs: id: check - name: Send Slack announcement of pipeline failure if: steps.check.outputs.status == 'failure' && github.event_name == 'schedule' - uses: slackapi/slack-github-action@v1.24.0 + uses: slackapi/slack-github-action@e28cf165c92ffef168d23c5c9000cffc8a25e117 # v1.24.0 with: payload: '{"url":"https://github.com/neo4j/graphql/actions/runs/${{ github.run_id }}"}' env: diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 45d917564c6..80d205f614b 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -13,6 +13,6 @@ jobs: runs-on: ubuntu-latest steps: - - uses: srvaroa/labeler@c6b5a7f36f14b184378092f75437bfd2b9facb97 # v1.4 + - uses: srvaroa/labeler@74404350883f8b689b026d8747622bd12d3f070a # v1.8.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 ce65c7cbe20..a2ccf0684ab 100644 --- a/.github/workflows/lint-github-actions.yml +++ b/.github/workflows/lint-github-actions.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - uses: reviewdog/action-actionlint@82693e9e3b239f213108d6e412506f8b54003586 # v1.39.1 with: reporter: github-check diff --git a/.github/workflows/lint-markdown.yml b/.github/workflows/lint-markdown.yml index f0824f31a6f..432fe1ccc78 100644 --- a/.github/workflows/lint-markdown.yml +++ b/.github/workflows/lint-markdown.yml @@ -13,8 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* - name: Install markdownlint diff --git a/.github/workflows/performance-tests-comment.yml b/.github/workflows/performance-tests-comment.yml index bca04cf6000..6e9278a9b65 100644 --- a/.github/workflows/performance-tests-comment.yml +++ b/.github/workflows/performance-tests-comment.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Download performance report" - uses: actions/github-script@v7.0.1 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ diff --git a/.github/workflows/performance-tests.yml b/.github/workflows/performance-tests.yml index 3a1d2f73c16..d404e7a2899 100644 --- a/.github/workflows/performance-tests.yml +++ b/.github/workflows/performance-tests.yml @@ -24,12 +24,12 @@ jobs: steps: - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* - name: Target Branch - Check out - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: ref: ${{ github.base_ref }} fetch-depth: 0 @@ -71,7 +71,7 @@ jobs: NEO_URL: bolt://localhost:7687 - name: PR Branch - Check out - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: ref: ${{ github.ref }} clean: false @@ -119,7 +119,7 @@ jobs: working-directory: packages/graphql - name: Archive performance test results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: performance path: packages/graphql/performance/ diff --git a/.github/workflows/post-federation-test-results.yml b/.github/workflows/post-federation-test-results.yml index 46d56f2fc22..ea16f2b3932 100644 --- a/.github/workflows/post-federation-test-results.yml +++ b/.github/workflows/post-federation-test-results.yml @@ -14,7 +14,7 @@ jobs: steps: - name: "Download Apollo Federation Subgraph Compatibility results" - uses: actions/github-script@v7.0.1 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index bc1b08e7117..f896438ec48 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -17,8 +17,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: 18.13.0 cache: yarn @@ -44,7 +44,7 @@ jobs: typescript_files: ${{ steps.filter.outputs.typescript_files }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - uses: dorny/paths-filter@4512585405083f25c027a35db413c2b3b9006d50 # tag=v2.11.1 id: filter with: @@ -60,8 +60,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4 with: go-version: "^1.17.0" - name: Install addlicense diff --git a/.github/workflows/reusable-api-library-tests.yml b/.github/workflows/reusable-api-library-tests.yml index aa9c9cb5c82..9098da99311 100644 --- a/.github/workflows/reusable-api-library-tests.yml +++ b/.github/workflows/reusable-api-library-tests.yml @@ -47,8 +47,8 @@ jobs: - 7687:7687 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* cache: yarn @@ -63,7 +63,7 @@ jobs: NEO_USER: neo4j - if: ${{ env.CODECOV_TOKEN != '' && !inputs.disable-code-cov-upload }} name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./packages/graphql/coverage/ @@ -84,8 +84,8 @@ jobs: - 7687:7687 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* cache: yarn @@ -95,7 +95,7 @@ jobs: run: yarn --cwd packages/graphql run test e2e --coverage - if: ${{ env.CODECOV_TOKEN != '' }} name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./packages/graphql/coverage/ @@ -106,8 +106,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* cache: yarn @@ -117,7 +117,7 @@ jobs: run: yarn --cwd packages/graphql run test:schema --coverage - if: ${{ env.CODECOV_TOKEN != '' && !inputs.disable-code-cov-upload }} name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./packages/graphql/coverage/ @@ -125,7 +125,7 @@ jobs: fail_ci_if_error: true - if: ${{ env.CODECOV_TOKEN != '' && !inputs.disable-code-cov-upload }} name: Archive coverage report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: api-library-coverage-graphql path: packages/graphql/coverage/ diff --git a/.github/workflows/reusable-aura-tests.yml b/.github/workflows/reusable-aura-tests.yml index 2d6c696c8a8..9ca743d6ce8 100644 --- a/.github/workflows/reusable-aura-tests.yml +++ b/.github/workflows/reusable-aura-tests.yml @@ -26,10 +26,10 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: ref: ${{ inputs.BRANCH || github.ref }} - - uses: actions/setup-node@v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* cache: yarn @@ -63,8 +63,8 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* cache: yarn diff --git a/.github/workflows/reusable-codeql-analysis.yml b/.github/workflows/reusable-codeql-analysis.yml index 6337769ee68..72717506d99 100644 --- a/.github/workflows/reusable-codeql-analysis.yml +++ b/.github/workflows/reusable-codeql-analysis.yml @@ -9,14 +9,14 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2 with: config-file: ./.github/codeql/codeql-config.yml languages: javascript - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2 diff --git a/.github/workflows/reusable-federation-tests.yml b/.github/workflows/reusable-federation-tests.yml index 9f13911e24b..dc59fe2c450 100644 --- a/.github/workflows/reusable-federation-tests.yml +++ b/.github/workflows/reusable-federation-tests.yml @@ -9,8 +9,8 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* cache: yarn @@ -37,7 +37,7 @@ jobs: mkdir prnumber echo "$PULL_REQUEST_NUMBER" > ./prnumber/prnumber - name: Archive PR number - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: prnumber path: prnumber/ diff --git a/.github/workflows/reusable-integration-tests-on-prem-nightly.yml b/.github/workflows/reusable-integration-tests-on-prem-nightly.yml index 291d394aac8..f2d06131ab1 100644 --- a/.github/workflows/reusable-integration-tests-on-prem-nightly.yml +++ b/.github/workflows/reusable-integration-tests-on-prem-nightly.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Login to ECR - uses: aws-actions/amazon-ecr-login@v2 + uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2 id: login-to-ecr env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -76,14 +76,14 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - name: Setting up Node.js with version ${{ matrix.node }} - uses: actions/setup-node@v4 + uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: ${{ matrix.node }} cache: yarn - name: Login to ECR - uses: docker/login-action@v3 + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3 with: registry: 535893049302.dkr.ecr.eu-west-1.amazonaws.com username: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -116,7 +116,7 @@ jobs: NEO_URL: neo4j://localhost:7687 - if: ${{ env.CODECOV_TOKEN != '' && !inputs.disable-code-cov-upload && matrix.packages.package == 'graphql' }} name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./packages/${{ matrix.packages.package }}/coverage-nightly/ @@ -124,7 +124,7 @@ jobs: fail_ci_if_error: true - if: ${{ env.CODECOV_TOKEN != '' && !inputs.disable-code-cov-upload && matrix.packages.package == 'graphql' }} name: Archive coverage report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: integration-nightly-coverage-${{ matrix.packages.package }} path: packages/${{ matrix.packages.package }}/coverage/ diff --git a/.github/workflows/reusable-integration-tests-on-prem.yml b/.github/workflows/reusable-integration-tests-on-prem.yml index 974cade1447..124f7a1815d 100644 --- a/.github/workflows/reusable-integration-tests-on-prem.yml +++ b/.github/workflows/reusable-integration-tests-on-prem.yml @@ -52,8 +52,8 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* cache: yarn @@ -72,7 +72,7 @@ jobs: NEO_URL: bolt://localhost:7687 - if: ${{ env.CODECOV_TOKEN != '' && !inputs.disable-code-cov-upload && matrix.packages.package == 'graphql' }} name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./packages/${{ matrix.packages.package }}/coverage-${{ matrix.neo4j-version }}/ @@ -80,7 +80,7 @@ jobs: fail_ci_if_error: true - if: ${{ env.CODECOV_TOKEN != '' && !inputs.disable-code-cov-upload && matrix.packages.package == 'graphql' }} name: Archive coverage report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: integration-coverage-${{ matrix.packages.package }} path: packages/${{ matrix.packages.package }}/coverage/ diff --git a/.github/workflows/reusable-package-tests.yml b/.github/workflows/reusable-package-tests.yml index 54552607475..98fe65896f2 100644 --- a/.github/workflows/reusable-package-tests.yml +++ b/.github/workflows/reusable-package-tests.yml @@ -8,8 +8,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # 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 5e2ee2a22ba..9864bed2518 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 + image: rabbitmq@sha256:b669305108158abf3d7790a7f6f5a56b7de9598a926b6672281a7edf11460a2d env: RABBITMQ_DEFAULT_USER: guest RABBITMQ_DEFAULT_PASS: guest @@ -36,8 +36,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* cache: yarn @@ -56,7 +56,7 @@ jobs: RABBITMQ_USER: guest RABBITMQ_PASSWORD: guest - name: Archive coverage report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: e2e-coverage-graphql-amqp-subscriptions-engine path: packages/graphql-amqp-subscriptions-engine/coverage/ diff --git a/.github/workflows/reusable-toolbox-tests.yml b/.github/workflows/reusable-toolbox-tests.yml index a912151fa73..eef6527dfbf 100644 --- a/.github/workflows/reusable-toolbox-tests.yml +++ b/.github/workflows/reusable-toolbox-tests.yml @@ -19,8 +19,8 @@ jobs: steps: - name: Check out repository code - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* cache: yarn @@ -44,7 +44,7 @@ jobs: NEO_URL: bolt://localhost:7687 - name: Upload playwright report on failure if: failure() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: playwright-test-failure-report path: packages/graphql-toolbox/tests/artifacts diff --git a/.github/workflows/reusable-unit-tests.yml b/.github/workflows/reusable-unit-tests.yml index e0b5a4aad3f..01ca66b6eee 100644 --- a/.github/workflows/reusable-unit-tests.yml +++ b/.github/workflows/reusable-unit-tests.yml @@ -29,8 +29,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* cache: yarn @@ -41,7 +41,7 @@ jobs: working-directory: packages/${{ matrix.package }} - if: ${{ env.CODECOV_TOKEN != '' && !inputs.disable-code-cov-upload && matrix.package == 'graphql' }} name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3 with: token: ${{ secrets.CODECOV_TOKEN }} directory: ./packages/${{ matrix.package }}/coverage/ @@ -49,7 +49,7 @@ jobs: fail_ci_if_error: true - if: ${{ env.CODECOV_TOKEN != '' && !inputs.disable-code-cov-upload && matrix.package == 'graphql' }} name: Archive coverage report - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: unit-coverage-${{ matrix.package }} path: packages/${{ matrix.package }}/coverage/ diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 21a9dc9d959..e19ec08ecee 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -24,7 +24,7 @@ jobs: name: sonarcloud steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: Uppercase package name diff --git a/.github/workflows/toolbox-build.yml b/.github/workflows/toolbox-build.yml index 8eb347532ed..1ea45714b3c 100644 --- a/.github/workflows/toolbox-build.yml +++ b/.github/workflows/toolbox-build.yml @@ -13,8 +13,8 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* - name: Install dependencies @@ -31,7 +31,7 @@ jobs: echo "$PULL_REQUEST_NUMBER" > ./dist/prnumber working-directory: packages/graphql-toolbox - name: Archive Toolbox build - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 with: name: graphqltoolbox path: packages/graphql-toolbox/dist diff --git a/.github/workflows/toolbox-deploy.yml b/.github/workflows/toolbox-deploy.yml index 394f1eb8537..b76547eb176 100644 --- a/.github/workflows/toolbox-deploy.yml +++ b/.github/workflows/toolbox-deploy.yml @@ -15,8 +15,8 @@ jobs: environment: aws steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* - name: Install dependencies diff --git a/.github/workflows/toolbox-publish.yml b/.github/workflows/toolbox-publish.yml index b8a8722982a..8da0670ccfd 100644 --- a/.github/workflows/toolbox-publish.yml +++ b/.github/workflows/toolbox-publish.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Download built Toolbox - uses: actions/github-script@v7.0.1 + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 with: script: | var artifacts = await github.rest.actions.listWorkflowRunArtifacts({ @@ -39,7 +39,7 @@ jobs: number=$(> "$GITHUB_OUTPUT" - - uses: actions/setup-node@v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # 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 deda15a4e7c..6015fb2f185 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@v4 + - uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4 with: node-version: lts/* - name: Teardown graphql-toolbox diff --git a/Dockerfile b/Dockerfile index 796117ef63c..14f0cd34a35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.9.0-buster-slim +FROM node:20.10.0-buster-slim@sha256:b46831a79b7bd8d8d38b2bd50273b7611f0e168c840a97a1137a0cf243850086 WORKDIR /app diff --git a/examples/neo-place/docker-compose.yml b/examples/neo-place/docker-compose.yml index a79f6023c03..d578a9e8296 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 + image: rabbitmq:3.12-management@sha256:f8237ef62feb8fc61fe949170fa15379c45c784e1b8f1e9a4a0b9fbf7c5c8bf1 ports: - "5672:5672" - "15672:15672" diff --git a/examples/subscriptions/apollo_rabbitmq/docker-compose.yml b/examples/subscriptions/apollo_rabbitmq/docker-compose.yml index a79f6023c03..d578a9e8296 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 + image: rabbitmq:3.12-management@sha256:f8237ef62feb8fc61fe949170fa15379c45c784e1b8f1e9a4a0b9fbf7c5c8bf1 ports: - "5672:5672" - "15672:15672" diff --git a/package.json b/package.json index 40396dfd86a..d3e3c8f3a74 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,12 @@ }, "devDependencies": { "@tsconfig/node16": "1.0.4", - "@typescript-eslint/eslint-plugin": "6.12.0", - "@typescript-eslint/parser": "6.12.0", + "@typescript-eslint/eslint-plugin": "6.13.1", + "@typescript-eslint/parser": "6.13.1", "concurrently": "8.2.2", "dotenv": "16.3.1", - "eslint": "8.54.0", - "eslint-config-prettier": "9.0.0", + "eslint": "8.55.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", @@ -43,8 +43,8 @@ "graphql": "16.8.1", "husky": "8.0.3", "jest": "29.7.0", - "lint-staged": "15.1.0", - "neo4j-driver": "5.14.0", + "lint-staged": "15.2.0", + "neo4j-driver": "5.15.0", "npm-run-all": "4.1.5", "prettier": "2.8.8", "set-tz": "0.2.0", @@ -53,7 +53,7 @@ }, "packageManager": "yarn@3.7.0", "dependencies": { - "@changesets/changelog-github": "0.4.8", - "@changesets/cli": "2.26.2" + "@changesets/changelog-github": "0.5.0", + "@changesets/cli": "2.27.1" } } diff --git a/packages/apollo-federation-subgraph-compatibility/Dockerfile b/packages/apollo-federation-subgraph-compatibility/Dockerfile index 8222ecc5532..811732484d7 100644 --- a/packages/apollo-federation-subgraph-compatibility/Dockerfile +++ b/packages/apollo-federation-subgraph-compatibility/Dockerfile @@ -1,4 +1,4 @@ -FROM node:lts +FROM node:lts@sha256:445acd9b2ef7e9de665424053bf95652e0b8995ef36500557d48faf29300170a WORKDIR /app diff --git a/packages/apollo-federation-subgraph-compatibility/package.json b/packages/apollo-federation-subgraph-compatibility/package.json index 91ff474aa1d..b5eed4f3cd2 100644 --- a/packages/apollo-federation-subgraph-compatibility/package.json +++ b/packages/apollo-federation-subgraph-compatibility/package.json @@ -10,13 +10,13 @@ "dependencies": { "@apollo/server": "^4.7.0", "@graphql-tools/wrap": "^10.0.0", - "@neo4j/graphql": "^4.4.3", + "@neo4j/graphql": "^4.4.4", "graphql": "16.8.1", "graphql-tag": "^2.12.6", "neo4j-driver": "^5.8.0" }, "devDependencies": { - "@apollo/federation-subgraph-compatibility": "2.0.1", + "@apollo/federation-subgraph-compatibility": "2.1.0", "fork-ts-checker-webpack-plugin": "9.0.2", "ts-loader": "9.5.1", "tsconfig-paths-webpack-plugin": "4.1.0", diff --git a/packages/graphql-amqp-subscriptions-engine/docker-compose.yml b/packages/graphql-amqp-subscriptions-engine/docker-compose.yml index a6ec7d8f56d..fbbe8d8c20d 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 + image: rabbitmq:3.12-management@sha256:f8237ef62feb8fc61fe949170fa15379c45c784e1b8f1e9a4a0b9fbf7c5c8bf1 ports: - "5672:5672" - "15672:15672" diff --git a/packages/graphql-amqp-subscriptions-engine/package.json b/packages/graphql-amqp-subscriptions-engine/package.json index 438a49de91c..aa890ab7886 100644 --- a/packages/graphql-amqp-subscriptions-engine/package.json +++ b/packages/graphql-amqp-subscriptions-engine/package.json @@ -39,12 +39,12 @@ "@types/body-parser": "1.19.5", "@types/cors": "2.8.17", "@types/debug": "4.1.12", - "@types/jest": "29.5.9", - "@types/node": "20.9.3", + "@types/jest": "29.5.10", + "@types/node": "20.10.3", "camelcase": "6.3.0", "graphql-ws": "5.14.2", "jest": "29.7.0", - "neo4j-driver": "5.14.0", + "neo4j-driver": "5.15.0", "pluralize": "8.0.0", "randomstring": "1.3.0", "supertest": "6.3.3", diff --git a/packages/graphql-amqp-subscriptions-engine/qpid-docker/Dockerfile b/packages/graphql-amqp-subscriptions-engine/qpid-docker/Dockerfile index e9cb678d5c5..c23ed74a407 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 +FROM ibmjava:8-jre@sha256:956d16bfd2e8f53ab8a9aa0fe82f9750532f8b87a1cfcf6fc27bb3b24deb3726 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 96e8446dc8b..6895d7e984e 100644 --- a/packages/graphql-toolbox/CHANGELOG.md +++ b/packages/graphql-toolbox/CHANGELOG.md @@ -1,5 +1,12 @@ # @neo4j/graphql-toolbox +## 2.1.6 + +### Patch Changes + +- Updated dependencies [[`226e5ed`](https://github.com/neo4j/graphql/commit/226e5edd22d4bff0767392079bedb58313dd606d), [`24728fe`](https://github.com/neo4j/graphql/commit/24728fedd50a8176c54f67009b2afc84dd91418e), [`c09aa9b`](https://github.com/neo4j/graphql/commit/c09aa9bb1a6ee3d13f918b0fed483893055fb1f1), [`7b310d6`](https://github.com/neo4j/graphql/commit/7b310d6d150c788e04af64f69029740913ddffad), [`1bf0773`](https://github.com/neo4j/graphql/commit/1bf077318d0ddbf730edf53d635f507e36fc7374)]: + - @neo4j/graphql@4.4.4 + ## 2.1.5 ### Patch Changes diff --git a/packages/graphql-toolbox/package.json b/packages/graphql-toolbox/package.json index dd07e47a206..19cfd018540 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.5", + "version": "2.1.6", "description": "Developer UI For Neo4j GraphQL", "exports": "./dist/main.js", "main": "./dist/main.js", @@ -42,17 +42,17 @@ }, "author": "Neo4j", "dependencies": { - "@codemirror/autocomplete": "6.11.0", - "@codemirror/commands": "6.3.0", + "@codemirror/autocomplete": "6.11.1", + "@codemirror/commands": "6.3.2", "@codemirror/lang-javascript": "6.2.1", - "@codemirror/language": "6.9.2", + "@codemirror/language": "6.9.3", "@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.12", - "@neo4j/graphql": "4.4.3", + "@neo4j-ndl/react": "2.0.14", + "@neo4j/graphql": "4.4.4", "@neo4j/introspector": "2.0.0", "classnames": "2.3.2", "cm6-graphql": "0.0.12", @@ -62,22 +62,22 @@ "graphql": "16.8.1", "graphql-query-complexity": "0.12.0", "markdown-it": "13.0.2", - "neo4j-driver": "5.14.0", + "neo4j-driver": "5.15.0", "prettier": "3.0.0", "process": "0.11.10", "react": "18.2.0", "react-dom": "18.2.0", "thememirror": "2.0.1", - "zustand": "4.4.6" + "zustand": "4.4.7" }, "devDependencies": { - "@playwright/test": "1.40.0", + "@playwright/test": "1.40.1", "@tsconfig/create-react-app": "2.0.1", - "@types/codemirror": "5.60.14", + "@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.15", + "@types/react-dom": "18.2.17", "@types/webpack": "5.28.5", "autoprefixer": "10.4.16", "compression-webpack-plugin": "10.0.0", @@ -93,7 +93,7 @@ "jest-environment-jsdom": "29.7.0", "node-polyfill-webpack-plugin": "2.0.1", "parse5": "7.1.2", - "postcss": "8.4.31", + "postcss": "8.4.32", "postcss-loader": "7.3.3", "randomstring": "1.3.0", "style-loader": "3.3.3", diff --git a/packages/graphql/CHANGELOG.md b/packages/graphql/CHANGELOG.md index 8d72166bd5a..8c79fbfe432 100644 --- a/packages/graphql/CHANGELOG.md +++ b/packages/graphql/CHANGELOG.md @@ -1,5 +1,19 @@ # @neo4j/graphql +## 4.4.4 + +### Patch Changes + +- [#4247](https://github.com/neo4j/graphql/pull/4247) [`226e5ed`](https://github.com/neo4j/graphql/commit/226e5edd22d4bff0767392079bedb58313dd606d) Thanks [@darrellwarde](https://github.com/darrellwarde)! - Fix issue in authorization context generation. + +- [#4330](https://github.com/neo4j/graphql/pull/4330) [`24728fe`](https://github.com/neo4j/graphql/commit/24728fedd50a8176c54f67009b2afc84dd91418e) Thanks [@angrykoala](https://github.com/angrykoala)! - Update translation on fulltext to make it consistent for top level operations and phrase option + +- [#4144](https://github.com/neo4j/graphql/pull/4144) [`c09aa9b`](https://github.com/neo4j/graphql/commit/c09aa9bb1a6ee3d13f918b0fed483893055fb1f1) Thanks [@darrellwarde](https://github.com/darrellwarde)! - Include the `@subscriptionsAuthorization` `events` argument in validation. + +- [#4308](https://github.com/neo4j/graphql/pull/4308) [`7b310d6`](https://github.com/neo4j/graphql/commit/7b310d6d150c788e04af64f69029740913ddffad) Thanks [@mjfwebb](https://github.com/mjfwebb)! - Add filtering to interface aggregations + +- [#4309](https://github.com/neo4j/graphql/pull/4309) [`1bf0773`](https://github.com/neo4j/graphql/commit/1bf077318d0ddbf730edf53d635f507e36fc7374) Thanks [@MacondoExpress](https://github.com/MacondoExpress)! - Fix an authorization bug present for validation rules with a predicate against a nested field and the Connection API. https://github.com/neo4j/graphql/issues/4292. + ## 4.4.3 ### Patch Changes diff --git a/packages/graphql/package.json b/packages/graphql/package.json index 8f3d49d014f..dbdbd406a64 100644 --- a/packages/graphql/package.json +++ b/packages/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql", - "version": "4.4.3", + "version": "4.4.4", "description": "A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations", "keywords": [ "neo4j", @@ -49,19 +49,19 @@ }, "author": "Neo4j Inc.", "devDependencies": { - "@apollo/gateway": "2.5.7", + "@apollo/gateway": "2.6.1", "@apollo/server": "4.9.5", "@faker-js/faker": "8.3.1", "@types/deep-equal": "1.0.4", "@types/is-uuid": "1.0.2", - "@types/jest": "29.5.9", + "@types/jest": "29.5.10", "@types/jsonwebtoken": "9.0.5", - "@types/node": "20.9.3", + "@types/node": "20.10.3", "@types/pluralize": "0.0.33", "@types/randomstring": "1.1.11", - "@types/semver": "7.5.5", + "@types/semver": "7.5.6", "@types/supertest": "2.0.16", - "@types/ws": "8.5.9", + "@types/ws": "8.5.10", "dedent": "1.5.1", "graphql-middleware": "6.1.35", "graphql-tag": "2.12.6", @@ -75,7 +75,7 @@ "koa-router": "12.0.1", "libnpmsearch": "7.0.0", "mock-jwks": "1.0.10", - "nock": "13.3.8", + "nock": "13.4.0", "npm-run-all": "4.1.5", "randomstring": "1.3.0", "rimraf": "5.0.5", @@ -89,7 +89,7 @@ "@apollo/subgraph": "^2.2.3", "@graphql-tools/merge": "^9.0.0", "@graphql-tools/resolvers-composition": "^7.0.0", - "@graphql-tools/schema": "10.0.0", + "@graphql-tools/schema": "10.0.2", "@graphql-tools/utils": "^10.0.0", "@neo4j/cypher-builder": "^1.7.1", "camelcase": "^6.3.0", diff --git a/packages/graphql/src/graphql/directives/type-dependant-directives/static-definitions.ts b/packages/graphql/src/graphql/directives/type-dependant-directives/static-definitions.ts index 81c79361674..88b399c879b 100644 --- a/packages/graphql/src/graphql/directives/type-dependant-directives/static-definitions.ts +++ b/packages/graphql/src/graphql/directives/type-dependant-directives/static-definitions.ts @@ -73,6 +73,17 @@ export const AUTHENTICATION_OPERATION = new GraphQLEnumType({ }, }); +export const SUBSCRIPTIONS_AUTHORIZATION_FILTER_EVENT = new GraphQLEnumType({ + name: "SubscriptionsAuthorizationFilterEvent", + values: { + CREATED: { value: "CREATED" }, + UPDATED: { value: "UPDATED" }, + DELETED: { value: "DELETED" }, + RELATIONSHIP_CREATED: { value: "RELATIONSHIP_CREATED" }, + RELATIONSHIP_DELETED: { value: "RELATIONSHIP_DELETED" }, + }, +}); + export function getStaticAuthorizationDefinitions( JWTPayloadDefinition?: ObjectTypeDefinitionNode ): Array { @@ -81,11 +92,13 @@ export function getStaticAuthorizationDefinitions( const authorizationValidateOperation = astFromEnumType(AUTHORIZATION_VALIDATE_OPERATION, schema); const authorizationFilterOperation = astFromEnumType(AUTHORIZATION_FILTER_OPERATION, schema); const authenticationOperation = astFromEnumType(AUTHENTICATION_OPERATION, schema); + const subscriptionsAuthorizationFilterOperation = astFromEnumType(SUBSCRIPTIONS_AUTHORIZATION_FILTER_EVENT, schema); const ASTs: Array = [ authorizationValidateStage, authorizationValidateOperation, authorizationFilterOperation, authenticationOperation, + subscriptionsAuthorizationFilterOperation, ]; const JWTPayloadWhere = createJWTPayloadWhere(schema, JWTPayloadDefinition); diff --git a/packages/graphql/src/graphql/directives/type-dependant-directives/subscriptions-authorization.ts b/packages/graphql/src/graphql/directives/type-dependant-directives/subscriptions-authorization.ts index 63edda2d8e0..35005204d12 100644 --- a/packages/graphql/src/graphql/directives/type-dependant-directives/subscriptions-authorization.ts +++ b/packages/graphql/src/graphql/directives/type-dependant-directives/subscriptions-authorization.ts @@ -29,6 +29,7 @@ import { GraphQLSchema, GraphQLString, } from "graphql"; +import { SUBSCRIPTIONS_AUTHORIZATION_FILTER_EVENT } from "./static-definitions"; function createSubscriptionsAuthorizationWhere( typeDefinitionName: string, @@ -88,6 +89,10 @@ function createSubscriptionsAuthorizationFilterRule( name: `${typeDefinitionName}SubscriptionsAuthorizationFilterRule`, fields() { return { + events: { + type: new GraphQLList(SUBSCRIPTIONS_AUTHORIZATION_FILTER_EVENT), + defaultValue: ["CREATED", "UPDATED", "DELETED", "RELATIONSHIP_CREATED", "RELATIONSHIP_DELETED"], + }, requireAuthentication: { type: GraphQLBoolean, defaultValue: true, diff --git a/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts b/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts index 3ce9f8b8c41..0738b621b35 100644 --- a/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/FullTextAnnotation.ts @@ -18,9 +18,9 @@ */ export type FullTextField = { - name: string; + name?: string; fields: string[]; - queryName: string; + queryName?: string; indexName: string; }; diff --git a/packages/graphql/src/schema-model/annotation/SubscriptionsAuthorizationAnnotation.ts b/packages/graphql/src/schema-model/annotation/SubscriptionsAuthorizationAnnotation.ts index 6b8d74ab2f5..8c60f58a897 100644 --- a/packages/graphql/src/schema-model/annotation/SubscriptionsAuthorizationAnnotation.ts +++ b/packages/graphql/src/schema-model/annotation/SubscriptionsAuthorizationAnnotation.ts @@ -22,11 +22,11 @@ import type { GraphQLWhereArg } from "../../types"; export const SubscriptionsAuthorizationAnnotationArguments = ["filter"] as const; export const SubscriptionsAuthorizationFilterEventRule = [ - "CREATE", - "UPDATE", - "DELETE", - "CREATE_RELATIONSHIP", - "DELETE_RELATIONSHIP", + "CREATED", + "UPDATED", + "DELETED", + "RELATIONSHIP_CREATED", + "RELATIONSHIP_DELETED", ] as const; export type SubscriptionsAuthorizationFilterEvent = (typeof SubscriptionsAuthorizationFilterEventRule)[number]; diff --git a/packages/graphql/src/schema/resolvers/composition/utils/get-authorization-context.test.ts b/packages/graphql/src/schema/resolvers/composition/utils/get-authorization-context.test.ts new file mode 100644 index 00000000000..92cdc5dacbb --- /dev/null +++ b/packages/graphql/src/schema/resolvers/composition/utils/get-authorization-context.test.ts @@ -0,0 +1,69 @@ +/* + * 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 { createBearerToken } from "../../../../../tests/utils/create-bearer-token"; +import { Neo4jGraphQLAuthorization } from "../../../../classes/authorization/Neo4jGraphQLAuthorization"; +import { getAuthorizationContext } from "./get-authorization-context"; + +describe("getAuthorizationContext", () => { + const secret = "secret"; + + test("no authorization settings and token returns unauthorized context", async () => { + const context = await getAuthorizationContext({ token: createBearerToken(secret, { sub: "user" }) }, undefined); + expect(context.isAuthenticated).toBe(false); + }); + + test("no authorization settings and jwt returns authorized context", async () => { + const context = await getAuthorizationContext({ jwt: { sub: "user" } }, undefined); + expect(context.isAuthenticated).toBe(true); + expect(context.jwt?.sub).toBe("user"); + }); + + test("authorization settings but no jwt or token returns unauthorized context", async () => { + const context = await getAuthorizationContext({}, new Neo4jGraphQLAuthorization({ key: secret })); + expect(context.isAuthenticated).toBe(false); + }); + + test("decoded jwt returns authorized context", async () => { + const context = await getAuthorizationContext( + { jwt: { sub: "user" } }, + new Neo4jGraphQLAuthorization({ key: secret }) + ); + expect(context.isAuthenticated).toBe(true); + expect(context.jwt?.sub).toBe("user"); + }); + + test("token returns authorized context", async () => { + const context = await getAuthorizationContext( + { token: createBearerToken(secret, { sub: "user" }) }, + new Neo4jGraphQLAuthorization({ key: secret }) + ); + expect(context.isAuthenticated).toBe(true); + expect(context.jwt?.sub).toBe("user"); + }); + + test("decoded jwt and token returns authorized context using jwt", async () => { + const context = await getAuthorizationContext( + { jwt: { sub: "user1" }, token: createBearerToken(secret, { sub: "user2" }) }, + new Neo4jGraphQLAuthorization({ key: secret }) + ); + expect(context.isAuthenticated).toBe(true); + expect(context.jwt?.sub).toBe("user1"); + }); +}); diff --git a/packages/graphql/src/schema/resolvers/composition/utils/get-authorization-context.ts b/packages/graphql/src/schema/resolvers/composition/utils/get-authorization-context.ts index e044c792e9a..3793ec89585 100644 --- a/packages/graphql/src/schema/resolvers/composition/utils/get-authorization-context.ts +++ b/packages/graphql/src/schema/resolvers/composition/utils/get-authorization-context.ts @@ -28,12 +28,37 @@ import { debugObject } from "../../../../debug/debug-object"; const debug = Debug(DEBUG_AUTH); +const unauthorizedContext = { + isAuthenticated: false, + jwtParam: new Cypher.NamedParam("jwt", {}), + isAuthenticatedParam: new Cypher.NamedParam("isAuthenticated", false), +}; + export async function getAuthorizationContext( context: Neo4jGraphQLContext | Neo4jGraphQLSubscriptionsConnectionParams, authorization?: Neo4jGraphQLAuthorization, jwtClaimsMap?: Map ): Promise { - if (!context.jwt && authorization) { + if (context.jwt) { + const isAuthenticated = true; + const jwt = context.jwt; + + debugObject(debug, "using JWT provided in context", jwt); + + return { + isAuthenticated, + jwt, + jwtParam: new Cypher.NamedParam("jwt", jwt), + isAuthenticatedParam: new Cypher.NamedParam("isAuthenticated", isAuthenticated), + }; + } + + if (!authorization) { + debug("authorization settings not specified, request not authenticated"); + return unauthorizedContext; + } + + if (context.token) { const jwt = await authorization.decode(context); if (jwt) { context.jwt = jwt; @@ -48,28 +73,9 @@ export async function getAuthorizationContext( isAuthenticatedParam: new Cypher.NamedParam("isAuthenticated", isAuthenticated), claims: jwtClaimsMap, }; - } else { - const isAuthenticated = false; - - debug("request not authenticated"); - - return { - isAuthenticated, - jwtParam: new Cypher.NamedParam("jwt", {}), - isAuthenticatedParam: new Cypher.NamedParam("isAuthenticated", isAuthenticated), - }; } } - const isAuthenticated = true; - const jwt = context.jwt; - - debugObject(debug, "using JWT provided in context", jwt); - - return { - isAuthenticated, - jwt, - jwtParam: new Cypher.NamedParam("jwt", jwt), - isAuthenticatedParam: new Cypher.NamedParam("isAuthenticated", isAuthenticated), - }; + debug("request not authenticated"); + return unauthorizedContext; } diff --git a/packages/graphql/src/schema/resolvers/query/global-node.ts b/packages/graphql/src/schema/resolvers/query/global-node.ts index 64ad8d33ed8..520eb3d454a 100644 --- a/packages/graphql/src/schema/resolvers/query/global-node.ts +++ b/packages/graphql/src/schema/resolvers/query/global-node.ts @@ -71,7 +71,6 @@ export function globalNodeResolver({ nodes, entities }: { nodes: Node[]; entitie const { cypher, params } = translateRead({ context: context as Neo4jGraphQLTranslationContext, node, - isGlobalNode: true, entityAdapter, }); const executeResult = await execute({ diff --git a/packages/graphql/src/schema/resolvers/subscriptions/where/authorization.ts b/packages/graphql/src/schema/resolvers/subscriptions/where/authorization.ts index aff0ec89134..c30b957a1bf 100644 --- a/packages/graphql/src/schema/resolvers/subscriptions/where/authorization.ts +++ b/packages/graphql/src/schema/resolvers/subscriptions/where/authorization.ts @@ -17,9 +17,11 @@ * limitations under the License. */ +import type { SubscriptionsAuthorizationFilterEvent } from "../../../../schema-model/annotation/SubscriptionsAuthorizationAnnotation"; import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { SubscriptionsEvent } from "../../../../types"; import type { Neo4jGraphQLComposedSubscriptionsContext } from "../../composition/wrap-subscription"; +import type { SubscriptionEventType } from "../types"; import { filterByAuthorizationRules } from "./filters/filter-by-authorization-rules"; import { multipleConditionsAggregationMap } from "./utils/multiple-conditions-aggregation-map"; import { populateWhereParams } from "./utils/populate-where-params"; @@ -36,7 +38,7 @@ export function subscriptionAuthorization({ const subscriptionsAuthorization = entity.annotations.subscriptionsAuthorization; const matchedRules = (subscriptionsAuthorization?.filter || []).filter((rule) => - rule.events.some((e) => e.toLowerCase() === event.event) + rule.events.some((e) => authorizationEventMatchesEvent(e, event.event)) ); if (!matchedRules.length) { @@ -60,3 +62,21 @@ export function subscriptionAuthorization({ return multipleConditionsAggregationMap.OR(results); } + +function authorizationEventMatchesEvent( + authorizationEvent: SubscriptionsAuthorizationFilterEvent, + event: SubscriptionEventType +): boolean { + switch (authorizationEvent) { + case "CREATED": + return event === "create"; + case "UPDATED": + return event === "update"; + case "DELETED": + return event === "delete"; + case "RELATIONSHIP_CREATED": + return event === "create_relationship"; + case "RELATIONSHIP_DELETED": + return event === "delete_relationship"; + } +} diff --git a/packages/graphql/src/schema/validation/custom-rules/warnings/experimental-mode.ts b/packages/graphql/src/schema/validation/custom-rules/warnings/experimental-mode.ts new file mode 100644 index 00000000000..597804dfd1e --- /dev/null +++ b/packages/graphql/src/schema/validation/custom-rules/warnings/experimental-mode.ts @@ -0,0 +1,39 @@ +/* + * 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 { ASTVisitor } from "graphql"; + +export function WarnIfExperimentalMode(experimental: boolean) { + return function (): ASTVisitor { + let warningAlreadyIssued = false; + + return { + Document() { + if (experimental === true && !warningAlreadyIssued) { + console.warn( + "You have enabled experimental features of the Neo4j GraphQL Library. Be aware that experimental features may be incomplete and/or contain bugs, and that breaking changes may be introduced at any time." + ); + + warningAlreadyIssued = true; + } + return false; + }, + }; + }; +} diff --git a/packages/graphql/src/schema/validation/validate-document.test.ts b/packages/graphql/src/schema/validation/validate-document.test.ts index e7f5fac34d9..d3822746ac5 100644 --- a/packages/graphql/src/schema/validation/validate-document.test.ts +++ b/packages/graphql/src/schema/validation/validate-document.test.ts @@ -37,6 +37,68 @@ const additionalDefinitions = { objects: [] as ObjectTypeDefinitionNode[], }; +describe("experimental flag warning", () => { + let warn: jest.SpyInstance; + + beforeEach(() => { + warn = jest.spyOn(console, "warn").mockImplementation(() => {}); + }); + + afterEach(() => { + warn.mockReset(); + }); + + test("experimental warning happens when flag is true", () => { + const doc = gql` + type Movie { + id: ID + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type Actor { + name: String + } + `; + + validateDocument({ + document: doc, + additionalDefinitions, + features: {}, + experimental: true, + }); + + expect(warn).toHaveBeenCalledWith( + "You have enabled experimental features of the Neo4j GraphQL Library. Be aware that experimental features may be incomplete and/or contain bugs, and that breaking changes may be introduced at any time." + ); + expect(warn).toHaveBeenCalledOnce(); + }); + + test("experimental warning does not happen when flag is false", () => { + const doc = gql` + type Movie { + id: ID + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: OUT) + } + + type Actor { + name: String + } + `; + + validateDocument({ + document: doc, + additionalDefinitions, + features: {}, + experimental: true, + }); + + expect(warn).toHaveBeenCalledWith( + "You have enabled experimental features of the Neo4j GraphQL Library. Be aware that experimental features may be incomplete and/or contain bugs, and that breaking changes may be introduced at any time." + ); + expect(warn).toHaveBeenCalledOnce(); + }); +}); + describe("authorization warning", () => { let warn: jest.SpyInstance; @@ -158,7 +220,10 @@ describe("default max limit bypass warning", () => { experimental: true, }); - expect(warn).not.toHaveBeenCalled(); + expect(warn).toHaveBeenCalledOnce(); + expect(warn).toHaveBeenCalledWith( + "You have enabled experimental features of the Neo4j GraphQL Library. Be aware that experimental features may be incomplete and/or contain bugs, and that breaking changes may be introduced at any time." + ); }); test("max limit on concrete should trigger warning if no limit on interface", () => { @@ -179,10 +244,13 @@ describe("default max limit bypass warning", () => { experimental: true, }); + expect(warn).toHaveBeenCalledWith( + "You have enabled experimental features of the Neo4j GraphQL Library. Be aware that experimental features may be incomplete and/or contain bugs, and that breaking changes may be introduced at any time." + ); expect(warn).toHaveBeenCalledWith( "Max limit set on Movie may be bypassed by its interface Production. To fix this update the `@limit` max value on the interface type. Ignore this message if the behavior is intended!" ); - expect(warn).toHaveBeenCalledOnce(); + expect(warn).toHaveBeenCalledTimes(2); }); test("max limit lower on interface than concrete does not trigger warning", () => { @@ -203,7 +271,10 @@ describe("default max limit bypass warning", () => { experimental: true, }); - expect(warn).not.toHaveBeenCalled(); + expect(warn).toHaveBeenCalledWith( + "You have enabled experimental features of the Neo4j GraphQL Library. Be aware that experimental features may be incomplete and/or contain bugs, and that breaking changes may be introduced at any time." + ); + expect(warn).toHaveBeenCalledOnce(); }); test("Max limit higher on interface than concrete should trigger warning", () => { @@ -224,10 +295,13 @@ describe("default max limit bypass warning", () => { experimental: true, }); + expect(warn).toHaveBeenCalledWith( + "You have enabled experimental features of the Neo4j GraphQL Library. Be aware that experimental features may be incomplete and/or contain bugs, and that breaking changes may be introduced at any time." + ); expect(warn).toHaveBeenCalledWith( "Max limit set on Movie may be bypassed by its interface Production. To fix this update the `@limit` max value on the interface type. Ignore this message if the behavior is intended!" ); - expect(warn).toHaveBeenCalledOnce(); + expect(warn).toHaveBeenCalledTimes(2); }); test("Max limit higher on interface than concrete should not trigger warning if experimental: false", () => { @@ -248,7 +322,7 @@ describe("default max limit bypass warning", () => { experimental: false, }); - expect(warn).not.toHaveBeenCalledOnce(); + expect(warn).not.toHaveBeenCalled(); }); test("Max limit higher on interface than concrete should trigger warning - multiple implementing types", () => { @@ -273,10 +347,13 @@ describe("default max limit bypass warning", () => { experimental: true, }); + expect(warn).toHaveBeenCalledWith( + "You have enabled experimental features of the Neo4j GraphQL Library. Be aware that experimental features may be incomplete and/or contain bugs, and that breaking changes may be introduced at any time." + ); expect(warn).toHaveBeenCalledWith( "Max limit set on Series may be bypassed by its interface Production. To fix this update the `@limit` max value on the interface type. Ignore this message if the behavior is intended!" ); - expect(warn).toHaveBeenCalledOnce(); + expect(warn).toHaveBeenCalledTimes(2); }); test("Max limit higher on interface than concrete should trigger warning - on both implementing types", () => { @@ -301,13 +378,16 @@ describe("default max limit bypass warning", () => { experimental: true, }); + expect(warn).toHaveBeenCalledWith( + "You have enabled experimental features of the Neo4j GraphQL Library. Be aware that experimental features may be incomplete and/or contain bugs, and that breaking changes may be introduced at any time." + ); expect(warn).toHaveBeenCalledWith( "Max limit set on Movie may be bypassed by its interface Production. To fix this update the `@limit` max value on the interface type. Ignore this message if the behavior is intended!" ); - expect(warn).toHaveBeenLastCalledWith( + expect(warn).toHaveBeenCalledWith( "Max limit set on Series may be bypassed by its interface Production. To fix this update the `@limit` max value on the interface type. Ignore this message if the behavior is intended!" ); - expect(warn).toHaveBeenCalledTimes(2); + expect(warn).toHaveBeenCalledTimes(3); }); test("Max limit on interface does not trigger warning if only default limit set on concrete", () => { @@ -332,7 +412,10 @@ describe("default max limit bypass warning", () => { experimental: true, }); - expect(warn).not.toHaveBeenCalledOnce(); + expect(warn).toHaveBeenCalledWith( + "You have enabled experimental features of the Neo4j GraphQL Library. Be aware that experimental features may be incomplete and/or contain bugs, and that breaking changes may be introduced at any time." + ); + expect(warn).toHaveBeenCalledOnce(); }); }); diff --git a/packages/graphql/src/schema/validation/validate-document.ts b/packages/graphql/src/schema/validation/validate-document.ts index 1019f64a16c..aefb7c1dbe6 100644 --- a/packages/graphql/src/schema/validation/validate-document.ts +++ b/packages/graphql/src/schema/validation/validate-document.ts @@ -64,6 +64,7 @@ import { ValidDirectiveAtFieldLocation } from "./custom-rules/directives/valid-d import { WarnIfAuthorizationFeatureDisabled } from "./custom-rules/warnings/authorization-feature-disabled"; import { WarnIfListOfListsFieldDefinition } from "./custom-rules/warnings/list-of-lists"; import { WarnIfAMaxLimitCanBeBypassedThroughInterface } from "./custom-rules/warnings/limit-max-can-be-bypassed"; +import { WarnIfExperimentalMode } from "./custom-rules/warnings/experimental-mode"; function filterDocument(document: DocumentNode): DocumentNode { const nodeNames = document.definitions @@ -211,6 +212,7 @@ function runValidationRulesOnFilteredDocument({ WarnIfAuthorizationFeatureDisabled(features?.authorization), WarnIfListOfListsFieldDefinition, WarnIfAMaxLimitCanBeBypassedThroughInterface(experimental), + WarnIfExperimentalMode(experimental), ], schema ); diff --git a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts index 5188995fa19..24fd7d6a6cb 100644 --- a/packages/graphql/src/translate/queryAST/ast/QueryAST.ts +++ b/packages/graphql/src/translate/queryAST/ast/QueryAST.ts @@ -34,15 +34,14 @@ export class QueryAST { this.operation = operation; } - public build(neo4jGraphQLContext: Neo4jGraphQLTranslationContext): Cypher.Clause { - const context = this.buildQueryASTContext(neo4jGraphQLContext); - + public build(neo4jGraphQLContext: Neo4jGraphQLTranslationContext, varName?: string): Cypher.Clause { + const context = this.buildQueryASTContext(neo4jGraphQLContext, varName); return Cypher.concat(...this.transpile(context).clauses); } // TODO: refactor other top level operations to use this method instead of build - public buildNew(neo4jGraphQLContext: Neo4jGraphQLTranslationContext): Cypher.Clause { - const context = this.buildQueryASTContext(neo4jGraphQLContext); + 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); @@ -57,9 +56,12 @@ export class QueryAST { return this.operation.transpile(context); } - public buildQueryASTContext(neo4jGraphQLContext: Neo4jGraphQLTranslationContext): QueryASTContext { + public buildQueryASTContext( + neo4jGraphQLContext: Neo4jGraphQLTranslationContext, + varName = "this" + ): QueryASTContext { const queryASTEnv = new QueryASTEnv(); - const returnVariable = new Cypher.NamedVariable("this"); + const returnVariable = new Cypher.NamedVariable(varName); const node = this.getTargetFromOperation(neo4jGraphQLContext); return new QueryASTContext({ target: node, diff --git a/packages/graphql/src/translate/queryAST/ast/fields/FulltextScoreField.ts b/packages/graphql/src/translate/queryAST/ast/fields/FulltextScoreField.ts new file mode 100644 index 00000000000..343070fc8b7 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/fields/FulltextScoreField.ts @@ -0,0 +1,42 @@ +/* + * 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 Cypher from "@neo4j/cypher-builder"; +import type { Variable } from "@neo4j/cypher-builder"; +import type { QueryASTNode } from "../QueryASTNode"; +import { Field } from "./Field"; + +export class FulltextScoreField extends Field { + private score: Cypher.Variable; + + constructor({ alias, score }: { alias: string; score: Cypher.Variable }) { + super(alias); + this.score = score; + } + + public getProjectionField(_variable: Variable): Record<"score", Cypher.Variable> { + return { + score: this.score, + }; + } + + public getChildren(): QueryASTNode[] { + return []; + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/filters/property-filters/FulltextScoreFilter.ts b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/FulltextScoreFilter.ts new file mode 100644 index 00000000000..ea400bc2c5a --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/filters/property-filters/FulltextScoreFilter.ts @@ -0,0 +1,56 @@ +/* + * 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"; + +/** A property which comparison has already been parsed into a Param */ +export class FulltextScoreFilter extends Filter { + private scoreVariable: Cypher.Variable; + private min?: number; + private max?: number; + + constructor({ scoreVariable, min, max }: { scoreVariable: Cypher.Variable; min?: number; max?: number }) { + super(); + this.scoreVariable = scoreVariable; + this.min = min; + this.max = max; + } + + public getChildren(): QueryASTNode[] { + return []; + } + + public getPredicate(_queryASTContext: QueryASTContext): Cypher.Predicate { + const predicates: Cypher.Predicate[] = []; + + if (this.max || this.max === 0) { + const maxPredicate = Cypher.lte(this.scoreVariable, new Cypher.Param(this.max)); + predicates.push(maxPredicate); + } + if (this.min || this.min === 0) { + const minPredicate = Cypher.gte(this.scoreVariable, new Cypher.Param(this.min)); + predicates.push(minPredicate); + } + + return Cypher.and(...predicates); + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/operations/AggregationOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/AggregationOperation.ts index 948519e38b2..867ffbccfaa 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/AggregationOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/AggregationOperation.ts @@ -29,6 +29,7 @@ import type { AggregationField } from "../fields/aggregation-fields/AggregationF import type { Filter } from "../filters/Filter"; import type { AuthorizationFilters } from "../filters/authorization-filters/AuthorizationFilters"; import type { Pagination } from "../pagination/Pagination"; +import type { EntitySelection } from "../selection/EntitySelection"; import type { Sort } from "../sort/Sort"; import type { OperationTranspileResult } from "./operations"; import { Operation } from "./operations"; @@ -36,6 +37,7 @@ import { Operation } from "./operations"; // TODO: somewhat dupe of readOperation export class AggregationOperation extends Operation { public readonly entity: ConcreteEntityAdapter | RelationshipAdapter; // TODO: normal entities + private selection: EntitySelection; protected directed: boolean; public fields: AggregationField[] = []; // Aggregation fields @@ -55,13 +57,16 @@ export class AggregationOperation extends Operation { constructor({ entity, directed = true, + selection, }: { entity: ConcreteEntityAdapter | RelationshipAdapter; directed?: boolean; + selection: EntitySelection; }) { super(); this.entity = entity; this.directed = directed; + this.selection = selection; } public setFields(fields: AggregationField[]) { @@ -76,8 +81,8 @@ export class AggregationOperation extends Operation { this.pagination = pagination; } - public setFilters(filters: Filter[]) { - this.filters = filters; + public addFilters(...filters: Filter[]) { + this.filters.push(...filters); } public addAuthFilters(...filter: AuthorizationFilters[]) { @@ -92,6 +97,7 @@ export class AggregationOperation extends Operation { ...this.filters, ...this.sortFields, ...this.authFilters, + this.selection, this.pagination, ]); } @@ -200,14 +206,14 @@ export class AggregationOperation extends Operation { const fieldSubqueries = this.fields.map((f) => { const returnVariable = new Cypher.Variable(); this.aggregationProjectionMap.set(f.getProjectionField(returnVariable)); - return this.createSubquery(f, pattern, operationContext.target, returnVariable, operationContext); + return this.createSubquery(f, pattern, returnVariable, context); }); const nodeMap = new Cypher.Map(); const nodeFieldSubqueries = this.nodeFields.map((f) => { const returnVariable = new Cypher.Variable(); nodeMap.set(f.getProjectionField(returnVariable)); - return this.createSubquery(f, pattern, operationContext.target, returnVariable, operationContext); + return this.createSubquery(f, pattern, returnVariable, context); }); if (nodeMap.size > 0) { @@ -216,12 +222,11 @@ export class AggregationOperation extends Operation { let edgeFieldSubqueries: Cypher.Clause[] = []; if (operationContext.relationship) { - const relVar = operationContext.relationship; const edgeMap = new Cypher.Map(); edgeFieldSubqueries = this.edgeFields.map((f) => { const returnVariable = new Cypher.Variable(); edgeMap.set(f.getProjectionField(returnVariable)); - return this.createSubquery(f, pattern, relVar, returnVariable, operationContext); + return this.createSubquery(f, pattern, returnVariable, context, "edge"); }); if (edgeMap.size > 0) { this.aggregationProjectionMap.set("edge", edgeMap); @@ -234,18 +239,21 @@ export class AggregationOperation extends Operation { private createSubquery( field: AggregationField, pattern: Cypher.Pattern, - target: Cypher.Variable, returnVariable: Cypher.Variable, - context: QueryASTContext + context: QueryASTContext, + target: "edge" | "node" = "node" ): Cypher.Clause { - const matchClause = new Cypher.Match(pattern); + const { selection: matchClause, nestedContext } = this.selection.apply(context); let extraSelectionWith: Cypher.With | undefined = undefined; - const nestedSubqueries = wrapSubqueriesInCypherCalls(context, this.getChildren(), [target]); - const filterPredicates = this.getPredicates(context); + const nestedSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.getChildren(), [nestedContext.target]); + const targetVar = target === "edge" ? nestedContext.relationship : nestedContext.target; + if (!targetVar) throw new Error("Edge not define in aggregations"); + + const filterPredicates = this.getPredicates(nestedContext); const selectionClauses = this.getChildren().flatMap((c) => { - return c.getSelection(context); + return c.getSelection(nestedContext); }); if (selectionClauses.length > 0 || nestedSubqueries.length > 0) { extraSelectionWith = new Cypher.With("*"); @@ -258,12 +266,13 @@ export class AggregationOperation extends Operation { matchClause.where(filterPredicates); } } - const ret = this.getFieldProjectionClause(target, returnVariable, field); + + const ret = this.getFieldProjectionClause(targetVar, returnVariable, field); let sortClause: Cypher.With | undefined; if (this.sortFields.length > 0 || this.pagination) { sortClause = new Cypher.With("*"); - this.addSortToClause(context, target, sortClause); + this.addSortToClause(nestedContext, targetVar, sortClause); } return Cypher.concat( diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts index 686b582b429..f4808d8ef00 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ConnectionReadOperation.ts @@ -17,24 +17,24 @@ * limitations under the License. */ -import { createNodeFromEntity } from "../../utils/create-node-from-entity"; -import type { Field } from "../fields/Field"; -import type { Filter } from "../filters/Filter"; import Cypher from "@neo4j/cypher-builder"; -import type { OperationTranspileResult } from "./operations"; -import { Operation } from "./operations"; -import type { Pagination, PaginationField } from "../pagination/Pagination"; -import type { Sort, SortField } from "../sort/Sort"; -import type { QueryASTContext } from "../QueryASTContext"; -import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import type { AuthorizationFilters } from "../filters/authorization-filters/AuthorizationFilters"; +import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { filterTruthy } from "../../../../utils/utils"; -import type { QueryASTNode } from "../QueryASTNode"; 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"; +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 { CypherPropertySort } from "../sort/CypherPropertySort"; -import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls"; +import type { Sort, SortField } from "../sort/Sort"; +import type { OperationTranspileResult } from "./operations"; +import { Operation } from "./operations"; export class ConnectionReadOperation extends Operation { public readonly relationship: RelationshipAdapter | undefined; @@ -68,8 +68,8 @@ export class ConnectionReadOperation extends Operation { this.nodeFields = fields; } - public setFilters(filters: Filter[]) { - this.filters = filters; + public addFilters(...filters: Filter[]) { + this.filters.push(...filters); } public setEdgeFields(fields: Field[]) { @@ -137,7 +137,7 @@ export class ConnectionReadOperation extends Operation { private transpileNested(context: QueryASTContext): OperationTranspileResult { if (!context.target || !this.relationship) throw new Error(); - const node = createNodeFromEntity(this.target, context.neo4jGraphQLContext); + const targetNode = createNodeFromEntity(this.target, context.neo4jGraphQLContext); const relationship = new Cypher.Relationship({ type: this.relationship.type }); const relDirection = this.relationship.getCypherDirection(this.directed); @@ -145,26 +145,28 @@ export class ConnectionReadOperation extends Operation { .withoutLabels() .related(relationship) .withDirection(relDirection) - .to(node); + .to(targetNode); - const nestedContext = context.push({ target: node, relationship }); + const nestedContext = context.push({ target: targetNode, relationship }); const { preSelection, selectionClause: clause } = this.getSelectionClauses(nestedContext, pattern); - const predicates = this.filters.map((f) => f.getPredicate(nestedContext)); - const authPredicate = this.getAuthFilterPredicate(nestedContext); + const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) => + new Cypher.Call(sq).innerWith(targetNode) + ); - const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext); + 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 nodeProjectionSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.nodeFields, [node]); const nodeProjectionMap = new Cypher.Map(); this.nodeFields - .map((f) => f.getProjectionField(node)) + .map((f) => f.getProjectionField(targetNode)) .forEach((p) => { if (typeof p === "string") { - nodeProjectionMap.set(p, node.property(p)); + nodeProjectionMap.set(p, targetNode.property(p)); } else { nodeProjectionMap.set(p); } @@ -174,7 +176,7 @@ export class ConnectionReadOperation extends Operation { const targetNodeName = this.target.name; nodeProjectionMap.set({ __resolveType: new Cypher.Literal(targetNodeName), - __id: Cypher.id(node), + __id: Cypher.id(targetNode), }); } @@ -216,9 +218,13 @@ export class ConnectionReadOperation extends Operation { let extraWithOrder: Cypher.Clause | undefined; if (this.sortFields.length > 0) { - const sortFields = this.getSortFields({ context: nestedContext, nodeVar: node, edgeVar: relationship }); + const sortFields = this.getSortFields({ + context: nestedContext, + nodeVar: targetNode, + edgeVar: relationship, + }); - extraWithOrder = new Cypher.With(relationship, node).orderBy(...sortFields); + extraWithOrder = new Cypher.With(relationship, targetNode).orderBy(...sortFields); } const projectionClauses = new Cypher.With([edgeProjectionMap, edgeVar]) @@ -254,19 +260,25 @@ export class ConnectionReadOperation extends Operation { if (this.relationship) { return this.transpileNested(context); } - if (!hasTarget(context)) throw new Error("No parent node found!"); + if (!hasTarget(context)) { + throw new Error("No parent node found!"); + } - const node = createNodeFromEntity(this.target, context.neo4jGraphQLContext, this.nodeAlias); + const targetNode = createNodeFromEntity(this.target, context.neo4jGraphQLContext, this.nodeAlias); - const { preSelection, selectionClause } = this.getSelectionClauses(context, node); + const { preSelection, selectionClause } = this.getSelectionClauses(context, targetNode); - const predicates = this.filters.map((f) => f.getPredicate(context)); - const authPredicate = this.getAuthFilterPredicate(context); + const { prePaginationSubqueries, postPaginationSubqueries } = this.getPreAndPostSubqueries(context); + + const authFilterSubqueries = this.getAuthFilterSubqueries(context).map((sq) => + new Cypher.Call(sq).innerWith(targetNode) + ); - const authFilterSubqueries = this.getAuthFilterSubqueries(context); + const authPredicate = this.getAuthFilterPredicate(context); + const predicates = this.filters.map((f) => f.getPredicate(context)); const filters = Cypher.and(...predicates, ...authPredicate); - const { prePaginationSubqueries, postPaginationSubqueries } = this.getPreAndPostSubqueries(context); + const nodeProjectionMap = this.getProjectionMap(context); const edgeVar = new Cypher.NamedVariable("edge"); @@ -286,12 +298,12 @@ export class ConnectionReadOperation extends Operation { selectionClause.where(filters); } } - const withNodeAndTotalCount = new Cypher.With([Cypher.collect(node), edgesVar]).with(edgesVar, [ + const withNodeAndTotalCount = new Cypher.With([Cypher.collect(targetNode), edgesVar]).with(edgesVar, [ Cypher.size(edgesVar), totalCount, ]); - const unwindClause = new Cypher.Unwind([edgesVar, node]).with(node, 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("*"); @@ -303,12 +315,12 @@ export class ConnectionReadOperation extends Operation { paginationWith.skip(paginationField.skip); } if (this.sortFields.length > 0) { - const sortFields = this.getSortFields({ context, nodeVar: node, aliased: true }); + const sortFields = this.getSortFields({ context, nodeVar: targetNode, aliased: true }); paginationWith.orderBy(...sortFields); } } - const withProjection = new Cypher.With([edgeProjectionMap, edgeVar], totalCount, node).with( + const withProjection = new Cypher.With([edgeProjectionMap, edgeVar], totalCount, targetNode).with( [Cypher.collect(edgeVar), edgesVar], totalCount ); diff --git a/packages/graphql/src/translate/queryAST/ast/operations/CreateOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/CreateOperation.ts index 39c052537dd..d3f00ad0ac2 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/CreateOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/CreateOperation.ts @@ -17,14 +17,14 @@ * limitations under the License. */ -import { filterTruthy } from "../../../../utils/utils"; import Cypher from "@neo4j/cypher-builder"; -import type { OperationTranspileResult } from "./operations"; -import { Operation } from "./operations"; -import type { QueryASTNode } from "../QueryASTNode"; import type { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import type { ReadOperation } from "./ReadOperation"; +import { filterTruthy } from "../../../../utils/utils"; import type { QueryASTContext } from "../QueryASTContext"; +import type { QueryASTNode } from "../QueryASTNode"; +import type { ReadOperation } from "./ReadOperation"; +import type { OperationTranspileResult } from "./operations"; +import { Operation } from "./operations"; /** * This is currently just a dummy tree node, diff --git a/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts new file mode 100644 index 00000000000..681edb2fce8 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/operations/FulltextOperation.ts @@ -0,0 +1,77 @@ +/* + * 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 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"; +import type { QueryASTContext } from "../QueryASTContext"; +import type { QueryASTNode } from "../QueryASTNode"; +import type { FulltextScoreField } from "../fields/FulltextScoreField"; +import type { EntitySelection } from "../selection/EntitySelection"; +import { ReadOperation } from "./ReadOperation"; + +export type FulltextOptions = { + index: string; + phrase: string; + score: Cypher.Variable; +}; + +export class FulltextOperation extends ReadOperation { + private scoreField: FulltextScoreField | undefined; + + constructor({ + target, + relationship, + directed, + scoreField, + selection, + }: { + target: ConcreteEntityAdapter; + relationship?: RelationshipAdapter; + directed?: boolean; + scoreField: FulltextScoreField | undefined; + selection: EntitySelection; + }) { + super({ + target, + directed, + relationship, + selection, + }); + + this.scoreField = scoreField; + } + + public getChildren(): QueryASTNode[] { + return filterTruthy([...super.getChildren(), this.scoreField]); + } + + protected getReturnStatement(context: QueryASTContext, returnVariable: Cypher.Variable): Cypher.Return { + const returnClause = super.getReturnStatement(context, returnVariable); + + if (this.scoreField) { + const scoreProjection = this.scoreField.getProjectionField(returnVariable); + + returnClause.addColumns([scoreProjection.score, "score"]); + } + + return returnClause; + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts index de6e022db2c..e862c760dd3 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/ReadOperation.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, createRelationshipFromEntity } from "../../utils/create-node-from-entity"; import { wrapSubqueriesInCypherCalls } from "../../utils/wrap-subquery-in-calls"; import type { QueryASTContext } from "../QueryASTContext"; import type { QueryASTNode } from "../QueryASTNode"; @@ -31,6 +30,7 @@ import { CypherAttributeField } from "../fields/attribute-fields/CypherAttribute import type { Filter } from "../filters/Filter"; import type { AuthorizationFilters } from "../filters/authorization-filters/AuthorizationFilters"; import type { Pagination } from "../pagination/Pagination"; +import type { EntitySelection, SelectionClause } from "../selection/EntitySelection"; import { CypherPropertySort } from "../sort/CypherPropertySort"; import type { Sort } from "../sort/Sort"; import type { OperationTranspileResult } from "./operations"; @@ -51,19 +51,25 @@ export class ReadOperation extends Operation { public nodeAlias: string | undefined; // This is just to maintain naming with the old way (this), remove after refactor + protected selection: EntitySelection; + constructor({ target, relationship, directed, + selection, }: { target: ConcreteEntityAdapter; relationship?: RelationshipAdapter; directed?: boolean; + selection: EntitySelection; }) { super(); this.target = target; this.directed = directed ?? true; this.relationship = relationship; + + this.selection = selection; } public setFields(fields: Field[]) { @@ -78,8 +84,8 @@ export class ReadOperation extends Operation { this.pagination = pagination; } - public setFilters(filters: Filter[]) { - this.filters = filters; + public addFilters(...filters: Filter[]) { + this.filters.push(...filters); } public addAuthFilters(...filter: AuthorizationFilters[]) { @@ -99,28 +105,27 @@ export class ReadOperation extends Operation { context: QueryASTContext ): OperationTranspileResult { const isCreateSelection = context.env.topLevelOperationName === "CREATE"; - //TODO: dupe from transpile if (!hasTarget(context)) throw new Error("No parent node found!"); - const relVar = createRelationshipFromEntity(entity); - const targetNode = createNodeFromEntity(entity.target, context.neo4jGraphQLContext); - const relDirection = entity.getCypherDirection(this.directed); - const pattern = new Cypher.Pattern(context.target) - .withoutLabels() - .related(relVar) - .withDirection(relDirection) - .to(targetNode); + // eslint-disable-next-line prefer-const + let { selection: matchClause, nestedContext } = this.selection.apply(context); - const nestedContext = context.push({ target: targetNode, relationship: relVar }); const filterPredicates = this.getPredicates(nestedContext); const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) => - new Cypher.Call(sq).innerWith(targetNode) + new Cypher.Call(sq).innerWith(nestedContext.target) ); const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext); - const { preSelection, selectionClause: matchClause } = this.getSelectionClauses(nestedContext, pattern); + 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; @@ -146,11 +151,11 @@ export class ReadOperation extends Operation { const cypherFieldSubqueries = this.getCypherFieldsSubqueries(nestedContext); const subqueries = Cypher.concat(...this.getFieldsSubqueries(nestedContext), ...cypherFieldSubqueries); - const sortSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.sortFields, [targetNode]); + const sortSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.sortFields, [nestedContext.target]); const ret = this.getProjectionClause(nestedContext, context.returnVariable, entity.isList); const clause = Cypher.concat( - ...preSelection, + ...extraMatches, matchClause, ...authFilterSubqueries, filterSubqueryWith, @@ -188,15 +193,15 @@ export class ReadOperation extends Operation { } protected getPredicates(queryASTContext: QueryASTContext): Cypher.Predicate | undefined { - return Cypher.and(...[...this.filters].map((f) => f.getPredicate(queryASTContext))); + 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; + preSelection: Array; + selectionClause: Cypher.Match | Cypher.With | Cypher.Yield; } { let matchClause: Cypher.Match | Cypher.With = new Cypher.Match(node); @@ -219,22 +224,45 @@ export class ReadOperation extends Operation { if (this.relationship) { return this.transpileNestedRelationship(this.relationship, context); } - const isCreateSelection = context.env.topLevelOperationName === "CREATE"; - const node = createNodeFromEntity(this.target, context.neo4jGraphQLContext, this.nodeAlias); - const filterSubqueries = wrapSubqueriesInCypherCalls(context, this.filters, [node]); - const filterPredicates = this.getPredicates(context); - const authFilterSubqueries = this.getAuthFilterSubqueries(context).map((sq) => - new Cypher.Call(sq).innerWith(node) + let { selection: matchClause, nestedContext } = this.selection.apply(context); + const isCreateSelection = nestedContext.env.topLevelOperationName === "CREATE"; + if (isCreateSelection) { + if (!context.hasTarget()) { + throw new Error("Invalid target for create operation"); + } + // Match is not applied on creation (last concat ignores the top level match) so we revert the context apply + nestedContext = context; + } + + const preWith: Cypher.With | undefined = + isCreateSelection && context.target ? new Cypher.With([context.target, nestedContext.target]) : undefined; + + const filterSubqueries = wrapSubqueriesInCypherCalls(nestedContext, this.filters, [nestedContext.target]); + const filterPredicates = this.getPredicates(nestedContext); + + const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext).map((sq) => + new Cypher.Call(sq).innerWith(nestedContext.target) ); - const fieldSubqueries = this.getFieldsSubqueries(context); - const cypherFieldSubqueries = this.getCypherFieldsSubqueries(context); - const sortSubqueries = wrapSubqueriesInCypherCalls(context, this.sortFields, [node]); + 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(context); - const ret: Cypher.Return = this.getReturnStatement(context, context.returnVariable); - const { preSelection, selectionClause: matchClause } = this.getSelectionClauses(context, node); + const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext); + const ret: Cypher.Return = this.getReturnStatement( + isCreateSelection ? context : nestedContext, + nestedContext.returnVariable + ); + + 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; @@ -261,7 +289,7 @@ export class ReadOperation extends Operation { let sortClause: Cypher.With | undefined; if (this.sortFields.length > 0 || this.pagination) { sortClause = new Cypher.With("*"); - this.addSortToClause(context, node, sortClause); + this.addSortToClause(nestedContext, nestedContext.target, sortClause); } const sortBlock = Cypher.concat(...sortSubqueries, sortClause); @@ -276,7 +304,8 @@ export class ReadOperation extends Operation { clause = Cypher.concat(filterSubqueriesClause, filterSubqueryWith, sortAndLimitBlock, subqueries, ret); } else { clause = Cypher.concat( - ...preSelection, + preWith, + ...extraMatches, matchClause, ...authFilterSubqueries, filterSubqueriesClause, @@ -293,7 +322,7 @@ export class ReadOperation extends Operation { }; } - private getReturnStatement(context: QueryASTContext, returnVariable): Cypher.Return { + protected getReturnStatement(context: QueryASTContext, returnVariable: Cypher.Variable): Cypher.Return { const projection = this.getProjectionMap(context); if (context.shouldCollect) { return new Cypher.Return([Cypher.collect(projection), returnVariable]); @@ -307,6 +336,7 @@ export class ReadOperation extends Operation { public getChildren(): QueryASTNode[] { return filterTruthy([ + this.selection, ...this.filters, ...this.authFilters, ...this.fields, diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeAggregationOperation.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeAggregationOperation.ts index 00ff8b9b921..9a748937ad8 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeAggregationOperation.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeAggregationOperation.ts @@ -35,9 +35,9 @@ import type { CompositeAggregationPartial } from "./CompositeAggregationPartial" export class CompositeAggregationOperation extends Operation { private children: CompositeAggregationPartial[]; protected directed: boolean; - public fields: AggregationField[] = []; // Aggregation fields - public nodeFields: AggregationField[] = []; // Aggregation node fields - public edgeFields: AggregationField[] = []; // Aggregation node fields + public fields: AggregationField[] = []; + public nodeFields: AggregationField[] = []; + public edgeFields: AggregationField[] = []; private entity: InterfaceEntityAdapter | UnionEntityAdapter; @@ -46,6 +46,10 @@ export class CompositeAggregationOperation extends Operation { protected filters: Filter[] = []; protected pagination: Pagination | undefined; protected sortFields: Sort[] = []; + private addWith: boolean = true; + private aggregationProjectionMap: Cypher.Map = new Cypher.Map(); + private nodeMap = new Cypher.Map(); + private edgeMap = new Cypher.Map(); public nodeAlias: string | undefined; // This is just to maintain naming with the old way (this), remove after refactor @@ -119,11 +123,9 @@ export class CompositeAggregationOperation extends Operation { public addPagination(pagination: Pagination): void { this.pagination = pagination; } - - public setFilters(filters: Filter[]) { - this.filters = filters; + public addFilters(...filters: Filter[]) { + this.filters.push(...filters); } - public addAuthFilters(...filter: AuthorizationFilters[]) { this.authFilters.push(...filter); } @@ -170,84 +172,91 @@ export class CompositeAggregationOperation extends Operation { return field.getAggregationProjection(target, returnVariable); } - private transpileAggregationOperation(context, addWith = true): OperationTranspileResult { - const aggregationProjectionMap: Cypher.Map = new Cypher.Map(); - const nodeMap = new Cypher.Map(); - const edgeMap = new Cypher.Map(); + private transpileAggregationOperation(context: QueryASTContext, addWith = true): OperationTranspileResult { + this.addWith = addWith; - const fieldSubqueries = this.fields.map((field) => { - const returnVariable = new Cypher.Variable(); - const nestedContext = context.setReturn(returnVariable); + const fieldSubqueries = this.createSubqueries(this.fields, context, this.aggregationProjectionMap); + const nodeFieldSubqueries = this.createSubqueries(this.nodeFields, context, this.nodeMap); + const edgeFieldSubqueries = this.createSubqueries( + this.edgeFields, + context, + this.edgeMap, + new Cypher.NamedNode("edge") + ); - const nestedSubquery = this.createSubquery(field, nestedContext, aggregationProjectionMap, addWith); + if (this.nodeMap.size > 0) { + this.aggregationProjectionMap.set("node", this.nodeMap); + } - const aggrProjection = field.getAggregationProjection( - nestedContext.returnVariable, - nestedContext.returnVariable - ); - return Cypher.concat(nestedSubquery, aggrProjection); - }); + if (this.edgeMap.size > 0) { + this.aggregationProjectionMap.set("edge", this.edgeMap); + } - const nodeFieldSubqueries = this.nodeFields.map((field) => { - const returnVariable = new Cypher.Variable(); + return { + clauses: [...fieldSubqueries, ...nodeFieldSubqueries, ...edgeFieldSubqueries], + projectionExpr: this.aggregationProjectionMap, + }; + } + private createSubqueries( + fields: AggregationField[], + context: QueryASTContext, + projectionMap: Cypher.Map, + target: Cypher.NamedNode = new Cypher.NamedNode("node") + ): Cypher.CompositeClause[] { + return fields.map((field) => { + const returnVariable = new Cypher.Node(); const nestedContext = context.setReturn(returnVariable); + const withClause: Cypher.With | undefined = this.createWithClause(context); - const nestedSubquery = this.createSubquery(field, nestedContext, nodeMap, addWith); - return Cypher.concat( - nestedSubquery, - field.getAggregationProjection(nestedContext.returnVariable, nestedContext.returnVariable) - ); - }); - - if (nodeMap.size > 0) { - aggregationProjectionMap.set("node", nodeMap); - } + const nestedSubquery = this.createNestedSubquery(nestedContext, target); - const edgeFieldSubqueries = this.edgeFields.map((field) => { - const returnVariable = new Cypher.Variable(); - const nestedContext = context.setReturn(returnVariable); + projectionMap.set(field.getProjectionField(nestedContext.returnVariable)); - const nestedSubquery = this.createSubquery(field, nestedContext, edgeMap, addWith, true); return Cypher.concat( nestedSubquery, - field.getAggregationProjection(nestedContext.returnVariable, nestedContext.returnVariable) + withClause, + field.getAggregationProjection(target, nestedContext.returnVariable) ); }); + } - if (edgeMap.size > 0) { - aggregationProjectionMap.set("edge", edgeMap); - } + private createWithClause(context: QueryASTContext): Cypher.With | undefined { + const node = new Cypher.NamedNode("node"); + const filterContext = new QueryASTContext({ + neo4jGraphQLContext: context.neo4jGraphQLContext, + target: node, + }); + const filterPredicates = this.getPredicates(filterContext); - return { - clauses: [...fieldSubqueries, ...nodeFieldSubqueries, ...edgeFieldSubqueries], - projectionExpr: aggregationProjectionMap, - }; + let withClause: Cypher.With | undefined; + if (filterPredicates) { + withClause = new Cypher.With("*"); + withClause.where(filterPredicates); + } + return withClause; } - private createSubquery( - field: AggregationField, - context: QueryASTContext, - addToMap: Cypher.Map, - addWith = true, - useRelationshipVariable = false - ) { + private createNestedSubquery(context: QueryASTContext, target: Cypher.NamedNode): Cypher.Call { const parentNode = context.target; const nestedSubqueries = this.children.flatMap((c) => { - c.setAttachedTo(useRelationshipVariable ? "relationship" : "node"); + if (target.name === "edge") { + c.setAttachedTo("relationship"); + } else { + c.setAttachedTo("node"); + } + const result = c.getSubqueries(context); let clauses = result; - if (parentNode && addWith) { + if (parentNode && this.addWith) { clauses = clauses.map((sq) => Cypher.concat(new Cypher.With(parentNode), sq)); } return clauses; }); - addToMap.set(field.getProjectionField(context.returnVariable)); - return new Cypher.Call(new Cypher.Union(...nestedSubqueries)); } } diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeAggregationPartial.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeAggregationPartial.ts index 45bc8e2168d..c6d175dad25 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeAggregationPartial.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeAggregationPartial.ts @@ -72,14 +72,26 @@ export class CompositeAggregationPartial extends QueryASTNode { .related(relVar) .withDirection(relDirection) .to(targetNode); + + const matchClause = new Cypher.Match(pattern); + + const nestedSubqueries = wrapSubqueriesInCypherCalls(context, this.getChildren(), [target]); + + return [ + Cypher.concat( + matchClause, + ...nestedSubqueries, + new Cypher.Return([targetNode, "node"], [relVar, "edge"]) + ), + ]; } else { pattern = new Cypher.Pattern(targetNode); - } - const matchClause = new Cypher.Match(pattern); + const matchClause = new Cypher.Match(pattern); - const nestedSubqueries = wrapSubqueriesInCypherCalls(context, this.getChildren(), [target]); + const nestedSubqueries = wrapSubqueriesInCypherCalls(context, this.getChildren(), [target]); - return [Cypher.concat(matchClause, ...nestedSubqueries, new Cypher.Return([target, context.returnVariable]))]; + return [Cypher.concat(matchClause, ...nestedSubqueries, new Cypher.Return([targetNode, "node"]))]; + } } public print(): string { diff --git a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts index 96e022b8b92..2b43ae83aea 100644 --- a/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts +++ b/packages/graphql/src/translate/queryAST/ast/operations/composite/CompositeReadPartial.ts @@ -20,8 +20,8 @@ import Cypher from "@neo4j/cypher-builder"; import type { RelationshipAdapter } from "../../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; import { hasTarget } from "../../../utils/context-has-target"; -import { createNodeFromEntity, createRelationshipFromEntity } from "../../../utils/create-node-from-entity"; -import { QueryASTContext } from "../../QueryASTContext"; +import type { QueryASTContext } from "../../QueryASTContext"; +import type { SelectionClause } from "../../selection/EntitySelection"; import { ReadOperation } from "../ReadOperation"; import type { OperationTranspileResult } from "../operations"; @@ -39,19 +39,19 @@ export class CompositeReadPartial extends ReadOperation { context: QueryASTContext ): OperationTranspileResult { if (!hasTarget(context)) throw new Error("No parent node found!"); - const parentNode = context.target; - const relVar = createRelationshipFromEntity(entity); - const targetNode = createNodeFromEntity(this.target, context.neo4jGraphQLContext); - const relDirection = entity.getCypherDirection(this.directed); - - const pattern = new Cypher.Pattern(parentNode) - .withoutLabels() - .related(relVar) - .withDirection(relDirection) - .to(targetNode); - - const nestedContext = context.push({ target: targetNode, relationship: relVar }); - const { preSelection, selectionClause: matchClause } = this.getSelectionClauses(nestedContext, pattern); + + // eslint-disable-next-line prefer-const + let { selection: matchClause, nestedContext } = this.selection.apply(context); + + let extraMatches: SelectionClause[] = this.getChildren().flatMap((f) => { + return f.getSelection(nestedContext); + }); + + if (extraMatches.length > 0) { + extraMatches = [matchClause, ...extraMatches]; + matchClause = new Cypher.With("*"); + } + const filterPredicates = this.getPredicates(nestedContext); const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext); const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext); @@ -66,12 +66,12 @@ export class CompositeReadPartial extends ReadOperation { const subqueries = Cypher.concat(...this.getFieldsSubqueries(nestedContext), ...cypherFieldSubqueries); const sortSubqueries = this.sortFields .flatMap((sq) => sq.getSubqueries(nestedContext)) - .map((sq) => new Cypher.Call(sq).innerWith(targetNode)); + .map((sq) => new Cypher.Call(sq).innerWith(nestedContext.target)); const ret = this.getProjectionClause(nestedContext, context.returnVariable); const clause = Cypher.concat( - ...preSelection, + ...extraMatches, matchClause, ...authFilterSubqueries, subqueries, @@ -87,13 +87,18 @@ export class CompositeReadPartial extends ReadOperation { // dupe from transpileNestedCompositeRelationship private transpileTopLevelCompositeEntity(context: QueryASTContext): OperationTranspileResult { - const targetNode = createNodeFromEntity(this.target, context.neo4jGraphQLContext); - const nestedContext = new QueryASTContext({ - target: targetNode, - env: context.env, - neo4jGraphQLContext: context.neo4jGraphQLContext, + // eslint-disable-next-line prefer-const + let { selection: matchClause, nestedContext } = this.selection.apply(context); + + let extraMatches: SelectionClause[] = this.getChildren().flatMap((f) => { + return f.getSelection(nestedContext); }); - const { preSelection, selectionClause: matchClause } = this.getSelectionClauses(nestedContext, targetNode); + + if (extraMatches.length > 0) { + extraMatches = [matchClause, ...extraMatches]; + matchClause = new Cypher.With("*"); + } + const filterPredicates = this.getPredicates(nestedContext); const authFilterSubqueries = this.getAuthFilterSubqueries(nestedContext); const authFiltersPredicate = this.getAuthFilterPredicate(nestedContext); @@ -105,7 +110,7 @@ export class CompositeReadPartial extends ReadOperation { const subqueries = Cypher.concat(...this.getFieldsSubqueries(nestedContext)); const ret = this.getProjectionClause(nestedContext, context.returnVariable); - const clause = Cypher.concat(...preSelection, matchClause, ...authFilterSubqueries, subqueries, ret); + const clause = Cypher.concat(...extraMatches, matchClause, ...authFilterSubqueries, subqueries, ret); return { clauses: [clause], diff --git a/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts new file mode 100644 index 00000000000..df06591b7ec --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/selection/EntitySelection.ts @@ -0,0 +1,36 @@ +/* + * 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 Cypher from "@neo4j/cypher-builder"; +import type { QueryASTContext } from "../QueryASTContext"; +import { QueryASTNode } from "../QueryASTNode"; + +export type SelectionClause = Cypher.Match | Cypher.With | Cypher.Yield; + +export abstract class EntitySelection extends QueryASTNode { + public getChildren(): QueryASTNode[] { + return []; + } + + /** Apply selection over the given context, returns the updated context and the selection clause */ + public abstract apply(context: QueryASTContext): { + nestedContext: QueryASTContext; + selection: SelectionClause; + }; +} diff --git a/packages/graphql/src/translate/queryAST/ast/selection/FulltextSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/FulltextSelection.ts new file mode 100644 index 00000000000..42f8de86865 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/selection/FulltextSelection.ts @@ -0,0 +1,79 @@ +/* + * 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 { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import { mapLabelsWithContext } from "../../../../schema-model/utils/map-labels-with-context"; +import { QueryASTContext } from "../QueryASTContext"; +import type { FulltextOptions } from "../operations/FulltextOperation"; +import { EntitySelection, type SelectionClause } from "./EntitySelection"; + +export class FulltextSelection extends EntitySelection { + private target: ConcreteEntityAdapter; + private fulltext: FulltextOptions; + + private scoreVariable: Cypher.Variable; + + constructor({ + target, + fulltext, + scoreVariable, + }: { + target: ConcreteEntityAdapter; + fulltext: FulltextOptions; + scoreVariable: Cypher.Variable; + }) { + super(); + this.target = target; + this.fulltext = fulltext; + this.scoreVariable = scoreVariable; + } + + public apply(context: QueryASTContext): { + nestedContext: QueryASTContext; + selection: SelectionClause; + } { + const node = new Cypher.Node(); + const phraseParam = new Cypher.Param(this.fulltext.phrase); + const indexName = new Cypher.Literal(this.fulltext.index); + + const fulltextClause: Cypher.Yield | Cypher.With = Cypher.db.index.fulltext + .queryNodes(indexName, phraseParam) + .yield(["node", node], ["score", this.scoreVariable]); + + const expectedLabels = mapLabelsWithContext(this.target.getLabels(), context.neo4jGraphQLContext); + + const whereOperators = expectedLabels.map((label) => { + return Cypher.in(new Cypher.Param(label), Cypher.labels(node)); + }); + + fulltextClause.where(Cypher.and(...whereOperators)); + + return { + selection: fulltextClause, + nestedContext: new QueryASTContext({ + target: node, + neo4jGraphQLContext: context.neo4jGraphQLContext, + returnVariable: context.returnVariable, + env: context.env, + shouldCollect: context.shouldCollect, + }), + }; + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/selection/NodeSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/NodeSelection.ts new file mode 100644 index 00000000000..23c3eb5c866 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/selection/NodeSelection.ts @@ -0,0 +1,53 @@ +/* + * 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 { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import { createNodeFromEntity } from "../../utils/create-node-from-entity"; +import { QueryASTContext } from "../QueryASTContext"; +import { EntitySelection, type SelectionClause } from "./EntitySelection"; + +export class NodeSelection extends EntitySelection { + private target: ConcreteEntityAdapter; + private alias: string | undefined; + + constructor({ target, alias }: { target: ConcreteEntityAdapter; alias?: string }) { + super(); + this.target = target; + this.alias = alias; + } + + public apply(context: QueryASTContext): { + nestedContext: QueryASTContext; + selection: SelectionClause; + } { + const node = createNodeFromEntity(this.target, context.neo4jGraphQLContext, this.alias); + + return { + selection: new Cypher.Match(node), + nestedContext: new QueryASTContext({ + target: node, + neo4jGraphQLContext: context.neo4jGraphQLContext, + returnVariable: context.returnVariable, + env: context.env, + shouldCollect: context.shouldCollect, + }), + }; + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts b/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts new file mode 100644 index 00000000000..991a80044d2 --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/selection/RelationshipSelection.ts @@ -0,0 +1,77 @@ +/* + * 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 { ConcreteEntityAdapter } from "../../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; +import type { RelationshipAdapter } from "../../../../schema-model/relationship/model-adapters/RelationshipAdapter"; +import { hasTarget } from "../../utils/context-has-target"; +import { createNodeFromEntity, createRelationshipFromEntity } from "../../utils/create-node-from-entity"; +import type { QueryASTContext } from "../QueryASTContext"; +import { EntitySelection, type SelectionClause } from "./EntitySelection"; + +export class RelationshipSelection extends EntitySelection { + private relationship: RelationshipAdapter; + // Overrides relationship target for composite entities + private targetOverride: ConcreteEntityAdapter | undefined; + private alias: string | undefined; + private directed: boolean; + + constructor({ + relationship, + alias, + directed, + targetOverride, + }: { + relationship: RelationshipAdapter; + alias?: string; + directed?: boolean; + targetOverride?: ConcreteEntityAdapter; + }) { + super(); + this.relationship = relationship; + this.alias = alias; + this.directed = directed ?? true; + this.targetOverride = targetOverride; + } + + public apply(context: QueryASTContext): { + nestedContext: QueryASTContext; + selection: SelectionClause; + } { + if (!hasTarget(context)) throw new Error("No parent node over a nested relationship match!"); + const relVar = createRelationshipFromEntity(this.relationship); + + const relationshipTarget = this.targetOverride ?? this.relationship.target; + const targetNode = createNodeFromEntity(relationshipTarget, context.neo4jGraphQLContext, this.alias); + const relDirection = this.relationship.getCypherDirection(this.directed); + + const pattern = new Cypher.Pattern(context.target) + .withoutLabels() + .related(relVar) + .withDirection(relDirection) + .to(targetNode); + + // NOTE: Direction not passed (can we remove it from context?) + const nestedContext = context.push({ target: targetNode, relationship: relVar }); + return { + nestedContext: nestedContext, + selection: new Cypher.Match(pattern), + }; + } +} diff --git a/packages/graphql/src/translate/queryAST/ast/sort/FulltextScoreSort.ts b/packages/graphql/src/translate/queryAST/ast/sort/FulltextScoreSort.ts new file mode 100644 index 00000000000..35b144dbded --- /dev/null +++ b/packages/graphql/src/translate/queryAST/ast/sort/FulltextScoreSort.ts @@ -0,0 +1,47 @@ +/* + * 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 Cypher from "@neo4j/cypher-builder"; +import type { QueryASTContext } from "../QueryASTContext"; +import type { QueryASTNode } from "../QueryASTNode"; +import type { SortField } from "./Sort"; +import { Sort } from "./Sort"; + +export class FulltextScoreSort extends Sort { + private direction: Cypher.Order; + private scoreVariable: Cypher.Variable; + + constructor({ scoreVariable, direction }: { scoreVariable: Cypher.Variable; direction: Cypher.Order }) { + super(); + this.scoreVariable = scoreVariable; + this.direction = direction; + } + + public getChildren(): QueryASTNode[] { + return []; + } + + public getSortFields(_context: QueryASTContext, _variable: Cypher.Variable | Cypher.Property): SortField[] { + return [[this.scoreVariable, this.direction]]; + } + + public getProjectionField(_context: QueryASTContext): string | Record { + return {}; + } +} diff --git a/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts b/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts index fa0a298f703..9e6a5abb308 100644 --- a/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/OperationFactory.ts @@ -18,7 +18,8 @@ */ import { mergeDeep } from "@graphql-tools/utils"; -import type { ResolveTree } from "graphql-parse-resolve-info"; +import * as Cypher from "@neo4j/cypher-builder"; +import type { FieldsByTypeName, ResolveTree } from "graphql-parse-resolve-info"; import { cursorToOffset } from "graphql-relay"; import { Integer } from "neo4j-driver"; import type { EntityAdapter } from "../../../schema-model/entity/EntityAdapter"; @@ -31,10 +32,14 @@ import type { AuthorizationOperation } from "../../../types/authorization"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { filterTruthy, isObject, isString } from "../../../utils/utils"; import { checkEntityAuthentication } from "../../authorization/check-authentication"; +import { FulltextScoreField } from "../ast/fields/FulltextScoreField"; import type { AuthorizationFilters } from "../ast/filters/authorization-filters/AuthorizationFilters"; +import { FulltextScoreFilter } from "../ast/filters/property-filters/FulltextScoreFilter"; import { AggregationOperation } from "../ast/operations/AggregationOperation"; import { ConnectionReadOperation } from "../ast/operations/ConnectionReadOperation"; import { CreateOperation } from "../ast/operations/CreateOperation"; +import type { FulltextOptions } from "../ast/operations/FulltextOperation"; +import { FulltextOperation } from "../ast/operations/FulltextOperation"; import { ReadOperation } from "../ast/operations/ReadOperation"; import { CompositeAggregationOperation } from "../ast/operations/composite/CompositeAggregationOperation"; import { CompositeAggregationPartial } from "../ast/operations/composite/CompositeAggregationPartial"; @@ -43,6 +48,10 @@ import { CompositeConnectionReadOperation } from "../ast/operations/composite/Co import { CompositeReadOperation } from "../ast/operations/composite/CompositeReadOperation"; import { CompositeReadPartial } from "../ast/operations/composite/CompositeReadPartial"; import type { Operation } from "../ast/operations/operations"; +import type { EntitySelection } from "../ast/selection/EntitySelection"; +import { FulltextSelection } from "../ast/selection/FulltextSelection"; +import { NodeSelection } from "../ast/selection/NodeSelection"; +import { RelationshipSelection } from "../ast/selection/RelationshipSelection"; import { getConcreteEntitiesInOnArgumentOfWhere } from "../utils/get-concrete-entities-in-on-argument-of-where"; import { getConcreteWhere } from "../utils/get-concrete-where"; import { isConcreteEntity } from "../utils/is-concrete-entity"; @@ -75,14 +84,37 @@ export class OperationsFactory { public createTopLevelOperation( entity: EntityAdapter | RelationshipAdapter, resolveTree: ResolveTree, - context: Neo4jGraphQLTranslationContext + context: Neo4jGraphQLTranslationContext, + varName?: string ): Operation { if (isConcreteEntity(entity)) { + // Handles deprecated top level fulltext + if (context.resolveTree.args.phrase) { + if (!context.fulltext) { + throw new Error("Failed to get context fulltext"); + } + const indexName = context.fulltext.indexName || context.fulltext.name; + if (indexName === undefined) { + throw new Error("The name of the fulltext index should be defined using the indexName argument."); + } + + const op = this.createFulltextOperation(entity, resolveTree, context); + op.nodeAlias = TOP_LEVEL_NODE_NAME; + return op; + } + const operationMatch = parseOperationField(resolveTree.name, entity); + if (operationMatch.isCreate) { return this.createCreateOperation(entity, resolveTree, context); // TODO: move this to separate method? } else if (operationMatch.isRead) { - const op = this.createReadOperation(entity, resolveTree, context) as ReadOperation; + let op: ReadOperation; + if (context.resolveTree.args.fulltext || context.resolveTree.args.phrase) { + op = this.createFulltextOperation(entity, resolveTree, context); + } else { + op = this.createReadOperation(entity, resolveTree, context, varName) as ReadOperation; + } + op.nodeAlias = TOP_LEVEL_NODE_NAME; return op; } else if (operationMatch.isConnection) { @@ -95,7 +127,7 @@ export class OperationsFactory { op.nodeAlias = TOP_LEVEL_NODE_NAME; return op; } else if (operationMatch.isAggregation) { - const op = this.createAggregationOperation(entity, resolveTree, context, true); + const op = this.createAggregationOperation(entity, resolveTree, context); op.nodeAlias = TOP_LEVEL_NODE_NAME; return op; } @@ -114,48 +146,101 @@ export class OperationsFactory { return this.createReadOperation(entity, resolveTree, context); } - // The current top-level Connection API is inconsistent with the rest of the API making the parsing more complex than it should be. - // This function temporary adjust some inconsistencies waiting for the new API. - // TODO: Remove it when the new API is ready. - private normalizeResolveTreeForTopLevelConnection(resolveTree: ResolveTree): ResolveTree { - const topLevelConnectionResolveTree = Object.assign({}, resolveTree); - // Move the sort arguments inside a "node" object. - if (topLevelConnectionResolveTree.args.sort) { - topLevelConnectionResolveTree.args.sort = (resolveTree.args.sort as any[]).map((sortField) => { - return { node: sortField }; - }); - } - // move the where arguments inside a "node" object. - if (topLevelConnectionResolveTree.args.where) { - topLevelConnectionResolveTree.args.where = { node: resolveTree.args.where }; - } - return topLevelConnectionResolveTree; - } - - private createCreateOperation( + public createFulltextOperation( entity: ConcreteEntityAdapter, resolveTree: ResolveTree, context: Neo4jGraphQLTranslationContext - ): CreateOperation { - const responseFields = Object.values( - resolveTree.fieldsByTypeName[entity.operations.mutationResponseTypeNames.create] ?? {} - ); - const createOP = new CreateOperation({ target: entity }); - const projectionFields = responseFields - .filter((f) => f.name === entity.plural) - .map((field) => { - const readOP = this.createReadOperation(entity, field, context) as ReadOperation; - return readOP; - }); + ): FulltextOperation { + let resolveTreeWhere: Record = isObject(resolveTree.args.where) ? resolveTree.args.where : {}; + let sortOptions: Record = (resolveTree.args.options as Record) || {}; + let fieldsByTypeName = resolveTree.fieldsByTypeName; + let resolverArgs = resolveTree.args; + const fulltextOptions = this.getFulltextOptions(context); + let scoreField: FulltextScoreField | undefined; + let scoreFilter: FulltextScoreFilter | undefined; + + // Compatibility of top level operations + const fulltextOperationDeprecatedFields = + resolveTree.fieldsByTypeName[entity.operations.fulltextTypeNames.result]; + + if (fulltextOperationDeprecatedFields) { + const scoreWhere = resolveTreeWhere.score; + resolveTreeWhere = resolveTreeWhere[entity.singular] || {}; + + const scoreRawField = fulltextOperationDeprecatedFields.score; + + const nestedResolveTree: Record = fulltextOperationDeprecatedFields[entity.singular] || {}; + resolverArgs = { ...(nestedResolveTree?.args || {}), ...resolveTree.args }; + + sortOptions = { + limit: sortOptions.limit, + offset: sortOptions.offset, + sort: filterTruthy((sortOptions.sort || []).map((field) => field[entity.singular] || field)), + }; + fieldsByTypeName = nestedResolveTree.fieldsByTypeName || {}; + if (scoreRawField) { + scoreField = this.createFulltextScoreField(scoreRawField, fulltextOptions.score); + } + if (scoreWhere) { + scoreFilter = new FulltextScoreFilter({ + scoreVariable: fulltextOptions.score, + min: scoreWhere.min, + max: scoreWhere.max, + }); + } + } - createOP.addProjectionOperations(projectionFields); - return createOP; + checkEntityAuthentication({ + entity: entity.entity, + targetOperations: ["READ"], + context, + }); + + const selection = new FulltextSelection({ + target: entity, + fulltext: fulltextOptions, + scoreVariable: fulltextOptions.score, + }); + const operation = new FulltextOperation({ + target: entity, + directed: Boolean(resolverArgs.directed ?? true), + scoreField, + selection, + }); + + if (scoreFilter) { + operation.addFilters(scoreFilter); + } + + this.hydrateOperation({ + operation, + entity, + fieldsByTypeName: fieldsByTypeName, + context, + whereArgs: resolveTreeWhere, + }); + + // Override sort to support score + const sortOptions2 = this.getOptions(entity, sortOptions); + + if (sortOptions2) { + const sort = this.sortAndPaginationFactory.createSortFields(sortOptions2, entity, fulltextOptions.score); + operation.addSort(...sort); + + const pagination = this.sortAndPaginationFactory.createPagination(sortOptions2); + if (pagination) { + operation.addPagination(pagination); + } + } + + return operation; } public createReadOperation( entityOrRel: EntityAdapter | RelationshipAdapter, resolveTree: ResolveTree, - context: Neo4jGraphQLTranslationContext + context: Neo4jGraphQLTranslationContext, + varName?: string ): ReadOperation | CompositeReadOperation { const entity = entityOrRel instanceof RelationshipAdapter ? entityOrRel.target : entityOrRel; const relationship = entityOrRel instanceof RelationshipAdapter ? entityOrRel : undefined; @@ -167,10 +252,25 @@ export class OperationsFactory { targetOperations: ["READ"], context, }); + + let selection: EntitySelection; + if (relationship) { + selection = new RelationshipSelection({ + relationship, + directed: Boolean(resolveTree.args?.directed ?? true), + }); + } else { + selection = new NodeSelection({ + target: entity, + alias: varName, + }); + } + const operation = new ReadOperation({ target: entity, relationship, directed: Boolean(resolveTree.args?.directed ?? true), + selection, }); return this.hydrateReadOperation({ @@ -183,10 +283,26 @@ export class OperationsFactory { } else { const concreteEntities = getConcreteEntitiesInOnArgumentOfWhere(entity, resolveTreeWhere); const concreteReadOperations = concreteEntities.map((concreteEntity: ConcreteEntityAdapter) => { + // Duplicate from normal read + let selection: EntitySelection; + if (relationship) { + selection = new RelationshipSelection({ + relationship, + directed: Boolean(resolveTree.args?.directed ?? true), + targetOverride: concreteEntity, + }); + } else { + selection = new NodeSelection({ + target: concreteEntity, + alias: varName, + }); + } + const readPartial = new CompositeReadPartial({ target: concreteEntity, relationship, directed: Boolean(resolveTree.args?.directed ?? true), + selection, }); const whereArgs = getConcreteWhere(entity, concreteEntity, resolveTreeWhere); @@ -214,9 +330,9 @@ export class OperationsFactory { public createAggregationOperation( entityOrRel: ConcreteEntityAdapter | RelationshipAdapter | InterfaceEntityAdapter, resolveTree: ResolveTree, - context: Neo4jGraphQLTranslationContext, - topLevel = false + context: Neo4jGraphQLTranslationContext ): AggregationOperation | CompositeAggregationOperation { + console.log("createAggregationOperation"); let entity: ConcreteEntityAdapter | InterfaceEntityAdapter; if (entityOrRel instanceof RelationshipAdapter) { entity = entityOrRel.target as ConcreteEntityAdapter; @@ -234,9 +350,15 @@ export class OperationsFactory { context, }); + const selection = new RelationshipSelection({ + relationship: entityOrRel, + directed: Boolean(resolveTree.args?.directed ?? true), + }); + const operation = new AggregationOperation({ entity: entityOrRel, directed: Boolean(resolveTree.args?.directed ?? true), + selection, }); return this.hydrateAggregationOperation({ @@ -278,9 +400,25 @@ export class OperationsFactory { } } else { if (isConcreteEntity(entity)) { + let selection: EntitySelection; + if (context.resolveTree.args.fulltext || context.resolveTree.args.phrase) { + const fulltextOptions = this.getFulltextOptions(context); + selection = new FulltextSelection({ + target: entity, + fulltext: fulltextOptions, + scoreVariable: fulltextOptions.score, + }); + } else { + selection = new NodeSelection({ + target: entity, + alias: "this", + }); + } + const operation = new AggregationOperation({ entity, directed: Boolean(resolveTree.args?.directed ?? true), + selection, }); //TODO: use a hydrate method here const rawProjectionFields = { @@ -326,7 +464,7 @@ export class OperationsFactory { const filters = this.filterFactory.createNodeFilters(entity, whereArgs); // Aggregation filters only apply to target node - operation.setFilters(filters); + operation.addFilters(...filters); if (authFilters.length > 0 || authValidate.length > 0) { operation.addAuthFilters(...authFilters); @@ -460,6 +598,81 @@ export class OperationsFactory { }); } + // The current top-level Connection API is inconsistent with the rest of the API making the parsing more complex than it should be. + // This function temporary adjust some inconsistencies waiting for the new API. + // TODO: Remove it when the new API is ready. + private normalizeResolveTreeForTopLevelConnection(resolveTree: ResolveTree): ResolveTree { + const topLevelConnectionResolveTree = Object.assign({}, resolveTree); + // Move the sort arguments inside a "node" object. + if (topLevelConnectionResolveTree.args.sort) { + topLevelConnectionResolveTree.args.sort = (resolveTree.args.sort as any[]).map((sortField) => { + return { node: sortField }; + }); + } + // move the where arguments inside a "node" object. + if (topLevelConnectionResolveTree.args.where) { + topLevelConnectionResolveTree.args.where = { node: resolveTree.args.where }; + } + return topLevelConnectionResolveTree; + } + + private createCreateOperation( + entity: ConcreteEntityAdapter, + resolveTree: ResolveTree, + context: Neo4jGraphQLTranslationContext + ): CreateOperation { + const responseFields = Object.values( + resolveTree.fieldsByTypeName[entity.operations.mutationResponseTypeNames.create] ?? {} + ); + const createOP = new CreateOperation({ target: entity }); + const projectionFields = responseFields + .filter((f) => f.name === entity.plural) + .map((field) => { + const readOP = this.createReadOperation(entity, field, context) as ReadOperation; + return readOP; + }); + + createOP.addProjectionOperations(projectionFields); + return createOP; + } + + private getFulltextOptions(context: Neo4jGraphQLTranslationContext): FulltextOptions { + if (context.fulltext) { + const indexName = context.fulltext.indexName || context.fulltext.name; + if (indexName === undefined) { + throw new Error("The name of the fulltext index should be defined using the indexName argument."); + } + const phrase = context.resolveTree.args.phrase; + if (!phrase || typeof phrase !== "string") { + throw new Error("Invalid phrase"); + } + + return { + index: indexName, + phrase, + score: context.fulltext.scoreVariable, + }; + } + + const entries = Object.entries(context.resolveTree.args.fulltext || {}); + if (entries.length > 1) { + throw new Error("Can only call one search at any given time"); + } + const [indexName, indexInput] = entries[0] as [string, { phrase: string }]; + return { + index: indexName, + phrase: indexInput.phrase, + score: new Cypher.Variable(), + }; + } + + private createFulltextScoreField(field: ResolveTree, scoreVar: Cypher.Variable): FulltextScoreField { + return new FulltextScoreField({ + alias: field.alias, + score: scoreVar, + }); + } + // eslint-disable-next-line @typescript-eslint/comma-dangle private hydrateConnectionOperationsASTWithSort< T extends ConnectionReadOperation | CompositeConnectionReadOperation @@ -572,7 +785,7 @@ export class OperationsFactory { operation.setNodeFields(nodeFields); operation.setEdgeFields(edgeFields); - operation.setFilters(filters); + operation.addFilters(...filters); if (authFilters) { operation.addAuthFilters(authFilters); } @@ -616,32 +829,38 @@ export class OperationsFactory { }; } - private hydrateReadOperation({ + private hydrateOperation({ entity, operation, - resolveTree, - context, whereArgs, + context, + sortArgs, + fieldsByTypeName, }: { entity: ConcreteEntityAdapter; operation: T; - resolveTree: ResolveTree; context: Neo4jGraphQLTranslationContext; whereArgs: Record; + sortArgs?: Record; + fieldsByTypeName: FieldsByTypeName; }): T { - let projectionFields = { ...resolveTree.fieldsByTypeName[entity.name] }; - + const concreteProjectionFields = { ...fieldsByTypeName[entity.name] }; // Get the abstract types of the interface const entityInterfaces = entity.compositeEntities; - const interfacesFields = filterTruthy(entityInterfaces.map((i) => resolveTree.fieldsByTypeName[i.name])); - - projectionFields = mergeDeep[]>([...interfacesFields, projectionFields]); + const interfacesFields = filterTruthy(entityInterfaces.map((i) => fieldsByTypeName[i.name])); + const projectionFields = mergeDeep[]>([ + ...interfacesFields, + concreteProjectionFields, + ]); const fields = this.fieldFactory.createFields(entity, projectionFields, context); + const filters = this.filterFactory.createNodeFilters(entity, whereArgs); + const authFilters = this.authorizationFactory.createEntityAuthFilters(entity, ["READ"], context); const authValidate = this.authorizationFactory.createEntityAuthValidate(entity, ["READ"], context, "BEFORE"); + const authAttributeFilters = this.createAttributeAuthFilters({ entity, context, @@ -654,10 +873,8 @@ export class OperationsFactory { when: "BEFORE", }); - const filters = this.filterFactory.createNodeFilters(entity, whereArgs); - operation.setFields(fields); - operation.setFilters(filters); + operation.addFilters(...filters); if (authFilters) { operation.addAuthFilters(authFilters); } @@ -670,11 +887,46 @@ export class OperationsFactory { if (authAttributeValidate) { operation.addAuthFilters(...authAttributeValidate); } - this.hydrateCompositeReadOperationWithPagination(entity, operation, resolveTree); + if (sortArgs) { + const sortOptions = this.getOptions(entity, sortArgs); + + if (sortOptions) { + const sort = this.sortAndPaginationFactory.createSortFields(sortOptions, entity); + operation.addSort(...sort); + + const pagination = this.sortAndPaginationFactory.createPagination(sortOptions); + if (pagination) { + operation.addPagination(pagination); + } + } + } return operation; } + private hydrateReadOperation({ + entity, + operation, + resolveTree, + context, + whereArgs, + }: { + entity: ConcreteEntityAdapter; + operation: T; + resolveTree: ResolveTree; + context: Neo4jGraphQLTranslationContext; + whereArgs: Record; + }): T { + return this.hydrateOperation({ + entity, + operation, + context, + whereArgs, + fieldsByTypeName: resolveTree.fieldsByTypeName, + sortArgs: (resolveTree.args.options as Record) || {}, + }); + } + private hydrateAggregationOperation({ relationship, entity, @@ -725,7 +977,7 @@ export class OperationsFactory { operation.setFields(fields); operation.setNodeFields(nodeFields); operation.setEdgeFields(edgeFields); - operation.setFilters(filters); + operation.addFilters(...filters); if (authFilters) { operation.addAuthFilters(authFilters); @@ -748,7 +1000,7 @@ export class OperationsFactory { ); const filters = this.filterFactory.createNodeFilters(entity, whereArgs); // Aggregation filters only apply to target node operation.setFields(fields); - operation.setFilters(filters); + operation.addFilters(...filters); if (authFilters) { operation.addAuthFilters(authFilters); @@ -772,7 +1024,10 @@ export class OperationsFactory { return operation; } - private getOptions(entity: EntityAdapter, options: Record): GraphQLOptionsArg | undefined { + private getOptions(entity: EntityAdapter, options?: Record): GraphQLOptionsArg | undefined { + if (!options) { + return undefined; + } const limitDirective = isUnionEntity(entity) ? undefined : entity.annotations.limit; let limit: Integer | number | undefined = options?.limit ?? limitDirective?.default ?? limitDirective?.max; diff --git a/packages/graphql/src/translate/queryAST/factory/QueryASTFactory.ts b/packages/graphql/src/translate/queryAST/factory/QueryASTFactory.ts index 61e08fcbbab..003c041723b 100644 --- a/packages/graphql/src/translate/queryAST/factory/QueryASTFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/QueryASTFactory.ts @@ -22,11 +22,11 @@ import type { Neo4jGraphQLSchemaModel } from "../../../schema-model/Neo4jGraphQL import type { EntityAdapter } from "../../../schema-model/entity/EntityAdapter"; import type { Neo4jGraphQLTranslationContext } from "../../../types/neo4j-graphql-translation-context"; import { QueryAST } from "../ast/QueryAST"; -import { OperationsFactory } from "./OperationFactory"; import { AuthFilterFactory } from "./AuthFilterFactory"; import { AuthorizationFactory } from "./AuthorizationFactory"; import { FieldFactory } from "./FieldFactory"; import { FilterFactory } from "./FilterFactory"; +import { OperationsFactory } from "./OperationFactory"; import { SortAndPaginationFactory } from "./SortAndPaginationFactory"; export class QueryASTFactory { @@ -53,8 +53,7 @@ export class QueryASTFactory { entityAdapter: EntityAdapter, context: Neo4jGraphQLTranslationContext ): QueryAST { - const operation = this.operationsFactory.createTopLevelOperation(entityAdapter, resolveTree, context); - + const operation = this.operationsFactory.createTopLevelOperation(entityAdapter, resolveTree, context, "this"); return new QueryAST(operation); } } diff --git a/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts b/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts index d7bfb688521..ccc97a988ea 100644 --- a/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts +++ b/packages/graphql/src/translate/queryAST/factory/SortAndPaginationFactory.ts @@ -17,6 +17,8 @@ * limitations under the License. */ +import type Cypher from "@neo4j/cypher-builder"; +import { SCORE_FIELD } from "../../../graphql/directives/fulltext"; import type { ConcreteEntityAdapter } from "../../../schema-model/entity/model-adapters/ConcreteEntityAdapter"; import type { InterfaceEntityAdapter } from "../../../schema-model/entity/model-adapters/InterfaceEntityAdapter"; import type { UnionEntityAdapter } from "../../../schema-model/entity/model-adapters/UnionEntityAdapter"; @@ -24,6 +26,7 @@ import type { RelationshipAdapter } from "../../../schema-model/relationship/mod import type { ConnectionSortArg, GraphQLOptionsArg, GraphQLSortArg } from "../../../types"; import { Pagination } from "../ast/pagination/Pagination"; import { CypherPropertySort } from "../ast/sort/CypherPropertySort"; +import { FulltextScoreSort } from "../ast/sort/FulltextScoreSort"; import { PropertySort } from "../ast/sort/PropertySort"; import type { Sort } from "../ast/sort/Sort"; import { isConcreteEntity } from "../utils/is-concrete-entity"; @@ -32,9 +35,12 @@ import { isUnionEntity } from "../utils/is-union-entity"; export class SortAndPaginationFactory { public createSortFields( options: GraphQLOptionsArg, - entity: ConcreteEntityAdapter | RelationshipAdapter | InterfaceEntityAdapter | UnionEntityAdapter + entity: ConcreteEntityAdapter | RelationshipAdapter | InterfaceEntityAdapter | UnionEntityAdapter, + scoreVariable?: Cypher.Variable ): Sort[] { - return (options.sort || [])?.flatMap((s) => this.createPropertySort(s, entity)); + return (options.sort || [])?.flatMap((s) => { + return this.createPropertySort(s, entity, scoreVariable); + }); } public createConnectionSortFields( @@ -67,13 +73,22 @@ export class SortAndPaginationFactory { private createPropertySort( optionArg: GraphQLSortArg, - entity: ConcreteEntityAdapter | InterfaceEntityAdapter | RelationshipAdapter | UnionEntityAdapter + entity: ConcreteEntityAdapter | InterfaceEntityAdapter | RelationshipAdapter | UnionEntityAdapter, + scoreVariable?: Cypher.Variable ): Sort[] { if (isUnionEntity(entity)) { return []; } return Object.entries(optionArg).map(([fieldName, sortDir]) => { + // TODO: fix conflict with a a "score" fieldname + if (fieldName === SCORE_FIELD && scoreVariable) { + return new FulltextScoreSort({ + scoreVariable, + direction: sortDir, + }); + } + const attribute = entity.findAttribute(fieldName); if (!attribute) throw new Error(`no filter attribute ${fieldName}`); if (attribute.annotations.cypher) { diff --git a/packages/graphql/src/translate/translate-aggregate.ts b/packages/graphql/src/translate/translate-aggregate.ts index 700e5ece779..de879f03377 100644 --- a/packages/graphql/src/translate/translate-aggregate.ts +++ b/packages/graphql/src/translate/translate-aggregate.ts @@ -17,18 +17,13 @@ * limitations under the License. */ -import Cypher from "@neo4j/cypher-builder"; +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 { BaseField, GraphQLWhereArg, PrimitiveField, TemporalField } from "../types"; -import { createAuthorizationBeforePredicateField } from "./authorization/create-authorization-before-predicate"; import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { compileCypher } from "../utils/compile-cypher"; -import { createDatetimeElement } from "./projection/elements/create-datetime-element"; import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; -import { translateTopLevelMatch } from "./translate-top-level-match"; const debug = Debug(DEBUG_TRANSLATE); @@ -60,145 +55,7 @@ function translateAggregate({ entityAdapter: EntityAdapter; }): Cypher.CypherResult { // TODO: Move fulltext to new translation layer to remove the deprecated translation - if (!context.resolveTree.args.fulltext && !context.resolveTree.args.phrase) { - return translateQuery({ context, entityAdapter }); - } - - if (!node) { - throw new Error("Translating Read: Node cannot be on aggregation."); - } - - const { fieldsByTypeName } = context.resolveTree; - const varName = "this"; - let cypherParams: Record = {}; - const cypherStrs: Cypher.Clause[] = []; - const matchNode = new Cypher.NamedNode(varName, { labels: node.getLabels(context) }); - const where = context.resolveTree.args.where as GraphQLWhereArg | undefined; - const topLevelMatch = translateTopLevelMatch({ matchNode, node, context, operation: "AGGREGATE", where }); - cypherStrs.push(new Cypher.RawCypher(topLevelMatch.cypher)); - cypherParams = { ...cypherParams, ...topLevelMatch.params }; - - const selections = fieldsByTypeName[node.aggregateTypeNames.selection] || {}; - const projections: Cypher.Map = new Cypher.Map(); - - // Do auth first so we can throw out before aggregating - Object.entries(selections).forEach((selection) => { - const authField = node.authableFields.find((x) => x.fieldName === selection[0]); - if (authField) { - const authorizationPredicateReturn = createAuthorizationBeforePredicateField({ - context, - nodes: [ - { - variable: new Cypher.NamedNode(varName), - node, - fieldName: authField.fieldName, - }, - ], - // This operation needs to be READ because this will actually return values, unlike the top-level AGGREGATE - operations: ["READ"], - }); - if (authorizationPredicateReturn) { - const { predicate, preComputedSubqueries } = authorizationPredicateReturn; - if (predicate) { - if (preComputedSubqueries && !preComputedSubqueries.empty) { - cypherStrs.push(preComputedSubqueries); - } - cypherStrs.push(new Cypher.With("*").where(predicate)); - } - } - } - }); - - Object.entries(selections).forEach((selection) => { - if (selection[1].name === "count") { - projections.set(`${selection[1].alias || selection[1].name}`, new Cypher.RawCypher(`count(${varName})`)); - } - - const primitiveField = node.primitiveFields.find((x) => x.fieldName === selection[1].name); - const temporalField = node.temporalFields.find((x) => x.fieldName === selection[1].name); - const field: BaseField = (primitiveField as PrimitiveField) || (temporalField as TemporalField); - let isDateTime = false; - const isString = primitiveField && primitiveField.typeMeta.name === "String"; - - if (!primitiveField && temporalField && temporalField.typeMeta.name === "DateTime") { - isDateTime = true; - } - - if (field) { - const thisProjections: Cypher.Expr[] = []; - const aggregateFields = - selection[1].fieldsByTypeName[`${field.typeMeta.name}AggregateSelectionNullable`] || - selection[1].fieldsByTypeName[`${field.typeMeta.name}AggregateSelectionNonNullable`] || - {}; - - Object.entries(aggregateFields).forEach((entry) => { - // "min" | "max" | "average" | "sum" | "shortest" | "longest" - let operator = entry[1].name; - - if (operator === "average") { - operator = "avg"; - } - - if (operator === "shortest") { - operator = "min"; - } - - if (operator === "longest") { - operator = "max"; - } - - const fieldName = field.dbPropertyName || field.fieldName; - - if (isDateTime) { - thisProjections.push( - createDatetimeElement({ - resolveTree: entry[1], - field: field as TemporalField, - variable: new Cypher.NamedVariable(varName), - valueOverride: `${operator}(this.${fieldName})`, - }) - ); - - return; - } - - if (isString) { - const lessOrGreaterThan = entry[1].name === "shortest" ? "<" : ">"; - - const reduce = ` - reduce(aggVar = collect(this.${fieldName})[0], current IN collect(this.${fieldName}) | - CASE - WHEN size(current) ${lessOrGreaterThan} size(aggVar) THEN current - ELSE aggVar - END - ) - `; - - thisProjections.push(new Cypher.RawCypher(`${entry[1].alias || entry[1].name}: ${reduce}`)); - - return; - } - - thisProjections.push( - new Cypher.RawCypher(`${entry[1].alias || entry[1].name}: ${operator}(this.${fieldName})`) - ); - }); - - projections.set( - `${selection[1].alias || selection[1].name}`, - Cypher.count(new Cypher.NamedVariable(varName)) - ); - projections.set( - `${selection[1].alias || selection[1].name}`, - new Cypher.RawCypher((env) => `{ ${thisProjections.map((p) => compileCypher(p, env)).join(", ")} }`) - ); - } - }); - - const retSt = new Cypher.Return(projections); - cypherStrs.push(retSt); - - return Cypher.concat(...cypherStrs).build(undefined, cypherParams); + 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 5944f0cec37..51ec0d2b5e3 100644 --- a/packages/graphql/src/translate/translate-create.ts +++ b/packages/graphql/src/translate/translate-create.ts @@ -17,20 +17,20 @@ * limitations under the License. */ -import type { Node } from "../classes"; +import Cypher from "@neo4j/cypher-builder"; import Debug from "debug"; -import createCreateAndParams from "./create-create-and-params"; +import type { Node } from "../classes"; +import { CallbackBucket } from "../classes/CallbackBucket"; import { DEBUG_TRANSLATE, META_CYPHER_VARIABLE } from "../constants"; +import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; +import { compileCypherIfExists } from "../utils/compile-cypher"; import { filterTruthy } from "../utils/utils"; -import { CallbackBucket } from "../classes/CallbackBucket"; -import Cypher from "@neo4j/cypher-builder"; -import unwindCreate from "./unwind-create"; import { UnsupportedUnwindOptimization } from "./batch-create/types"; -import { compileCypherIfExists } from "../utils/compile-cypher"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { getAuthorizationStatements } from "./utils/get-authorization-statements"; -import { QueryASTEnv, QueryASTContext } from "./queryAST/ast/QueryASTContext"; +import createCreateAndParams from "./create-create-and-params"; +import { QueryASTContext, QueryASTEnv } from "./queryAST/ast/QueryASTContext"; import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; +import unwindCreate from "./unwind-create"; +import { getAuthorizationStatements } from "./utils/get-authorization-statements"; const debug = Debug(DEBUG_TRANSLATE); diff --git a/packages/graphql/src/translate/translate-read.ts b/packages/graphql/src/translate/translate-read.ts index 15b8952cb81..f7bd764372b 100644 --- a/packages/graphql/src/translate/translate-read.ts +++ b/packages/graphql/src/translate/translate-read.ts @@ -17,251 +17,36 @@ * limitations under the License. */ -import Cypher from "@neo4j/cypher-builder"; +import type Cypher from "@neo4j/cypher-builder"; import Debug from "debug"; -import type { ResolveTree } from "graphql-parse-resolve-info"; -import { cursorToOffset } from "graphql-relay"; import type { Node } from "../classes"; import { DEBUG_TRANSLATE } from "../constants"; -import { SCORE_FIELD } from "../graphql/directives/fulltext"; import type { EntityAdapter } from "../schema-model/entity/EntityAdapter"; -import type { ConcreteEntityAdapter } from "../schema-model/entity/model-adapters/ConcreteEntityAdapter"; -import type { InterfaceEntityAdapter } from "../schema-model/entity/model-adapters/InterfaceEntityAdapter"; -import type { UnionEntityAdapter } from "../schema-model/entity/model-adapters/UnionEntityAdapter"; -import type { CypherFieldReferenceMap, GraphQLOptionsArg, GraphQLWhereArg } from "../types"; import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { compileCypher } from "../utils/compile-cypher"; -import createProjectionAndParams from "./create-projection-and-params"; -import { addSortAndLimitOptionsToClause } from "./projection/subquery/add-sort-and-limit-to-clause"; import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; -import { isConcreteEntity } from "./queryAST/utils/is-concrete-entity"; -import { createMatchClause } from "./translate-top-level-match"; const debug = Debug(DEBUG_TRANSLATE); -function translateQuery({ - context, - entityAdapter, -}: { - context: Neo4jGraphQLTranslationContext; - entityAdapter: EntityAdapter; -}): Cypher.CypherResult { - const { resolveTree } = context; - // TODO: Rename QueryAST to OperationsTree - const queryASTFactory = new QueryASTFactory(context.schemaModel); - - if (!entityAdapter) throw new Error("Entity not found"); - const queryAST = queryASTFactory.createQueryAST(resolveTree, entityAdapter, context); - debug(queryAST.print()); - const clause = queryAST.build(context); - return clause.build(); -} - -/** - * This function maintains the old behavior where the resolveTree in the context was mutated by the connection resolver, - * in the new way all resolvers will use the queryASTFactory which doesn't requires this anymore . - **/ -function getConnectionResolveTree({ - context, - entityAdapter, -}: { - context: Neo4jGraphQLTranslationContext; - entityAdapter: ConcreteEntityAdapter | UnionEntityAdapter | InterfaceEntityAdapter; -}): ResolveTree { - if (isConcreteEntity(entityAdapter)) { - const edgeTree = context.resolveTree.fieldsByTypeName[`${entityAdapter.upperFirstPlural}Connection`]?.edges; - const nodeTree = edgeTree?.fieldsByTypeName[`${entityAdapter.name}Edge`]?.node; - const resolveTreeForContext = nodeTree || context.resolveTree; - - return { - ...resolveTreeForContext, - args: context.resolveTree.args, - }; - } else { - throw new Error("Root connection fields are not yet supported for interfaces and unions."); - } -} - export function translateRead( { node, context, isRootConnectionField, - isGlobalNode, entityAdapter, }: { context: Neo4jGraphQLTranslationContext; node?: Node; isRootConnectionField?: boolean; - isGlobalNode?: boolean; entityAdapter: EntityAdapter; }, varName = "this" ): Cypher.CypherResult { - if (!context.resolveTree.args.fulltext && !context.resolveTree.args.phrase && !isGlobalNode) { - return translateQuery({ context, entityAdapter }); - } - if (isRootConnectionField) { - context.resolveTree = getConnectionResolveTree({ context, entityAdapter }); - } - const { resolveTree } = context; - if (!node) { - throw new Error("Translating Read: Node cannot be undefined."); - } - const matchNode = new Cypher.NamedNode(varName, { labels: node.getLabels(context) }); - - const cypherFieldAliasMap: CypherFieldReferenceMap = {}; - - const where = resolveTree.args.where as GraphQLWhereArg | undefined; - - let projAuth: Cypher.Clause | undefined; - - const { - matchClause: topLevelMatch, - preComputedWhereFieldSubqueries, - whereClause: topLevelWhereClause, - } = createMatchClause({ - matchNode, - node, - context, - operation: "READ", - where, - }); - - const projection = createProjectionAndParams({ - node, - context, - resolveTree, - varName: new Cypher.NamedNode(varName), - cypherFieldAliasMap, - }); - - const predicates: Cypher.Predicate[] = []; - - predicates.push(...projection.predicates); - - if (predicates.length) { - projAuth = new Cypher.With("*").where(Cypher.and(...predicates)); - } - - const projectionSubqueries = Cypher.concat(...projection.subqueries); - const projectionSubqueriesBeforeSort = Cypher.concat(...projection.subqueriesBeforeSort); + const operationsTreeFactory = new QueryASTFactory(context.schemaModel); - let orderClause: Cypher.Clause | Cypher.With | undefined; - - const optionsInput = (resolveTree.args.options || {}) as GraphQLOptionsArg; - - if (context.fulltext) { - optionsInput.sort = optionsInput.sort?.[node?.singular] || optionsInput.sort; - } - - if (node.limit) { - optionsInput.limit = node.limit.getLimit(optionsInput.limit); - resolveTree.args.options = resolveTree.args.options || {}; - (resolveTree.args.options as Record).limit = optionsInput.limit; - } - - const hasOrdering = optionsInput.sort || optionsInput.limit || optionsInput.offset; - - if (hasOrdering) { - orderClause = new Cypher.With("*"); - addSortAndLimitOptionsToClause({ - optionsInput, - target: matchNode, - projectionClause: orderClause as Cypher.With, - nodeField: node.singular, - fulltextScoreVariable: context.fulltext?.scoreVariable, - cypherFields: node.cypherFields, - cypherFieldAliasMap, - graphElement: node, - }); - } - - const projectionExpression = new Cypher.RawCypher((env) => { - return [`${varName} ${compileCypher(projection.projection, env)}`, projection.params]; - }); - - let returnClause = new Cypher.Return([projectionExpression, varName]); - - if (context.fulltext?.scoreVariable) { - returnClause = new Cypher.Return( - [projectionExpression, varName], - [context.fulltext?.scoreVariable, SCORE_FIELD] - ); - } - - let projectionClause: Cypher.Clause = returnClause; // TODO avoid reassign - let connectionPreClauses: Cypher.Clause | undefined; - - if (isRootConnectionField) { - const hasConnectionOrdering = resolveTree.args.first || resolveTree.args.after || resolveTree.args.sort; - if (hasConnectionOrdering) { - const afterInput = resolveTree.args.after as string | undefined; - const offset = afterInput ? cursorToOffset(afterInput) + 1 : undefined; - orderClause = new Cypher.With("*"); - addSortAndLimitOptionsToClause({ - optionsInput: { - sort: resolveTree.args.sort as any, - limit: resolveTree.args.first as any, - offset, - }, - target: matchNode, - projectionClause: orderClause as Cypher.With, - nodeField: node.singular, - fulltextScoreVariable: context.fulltext?.scoreVariable, - cypherFields: node.cypherFields, - cypherFieldAliasMap, - graphElement: node, - }); - } - - // TODO: unify with createConnectionClause - const edgesVar = new Cypher.NamedVariable("edges"); - const edgeVar = new Cypher.NamedVariable("edge"); - const totalCountVar = new Cypher.NamedVariable("totalCount"); - - const withCollect = new Cypher.With([Cypher.collect(matchNode), edgesVar]).with(edgesVar, [ - Cypher.size(edgesVar), - totalCountVar, - ]); - - const unwind = new Cypher.Unwind([edgesVar, matchNode]).with(matchNode, totalCountVar); - connectionPreClauses = Cypher.concat(withCollect, unwind); - - const connectionEdge = new Cypher.Map({ - node: projectionExpression, - }); - - const withTotalCount = new Cypher.With([connectionEdge, edgeVar], totalCountVar, matchNode); - const returnClause = new Cypher.With([Cypher.collect(edgeVar), edgesVar], totalCountVar).return([ - new Cypher.Map({ - edges: edgesVar, - totalCount: totalCountVar, - }), - matchNode, - ]); - - projectionClause = Cypher.concat(withTotalCount, returnClause); - } - - const preComputedWhereFields: Cypher.Clause | undefined = - preComputedWhereFieldSubqueries && !preComputedWhereFieldSubqueries.empty - ? Cypher.concat(preComputedWhereFieldSubqueries, topLevelWhereClause) - : topLevelWhereClause; - - const readQuery = Cypher.concat( - topLevelMatch, - preComputedWhereFields, - projAuth, - connectionPreClauses, - projectionSubqueriesBeforeSort, - orderClause, // Required for performance optimization - projectionSubqueries, - projectionClause - ); - - const result = readQuery.build(); - - return result; + if (!entityAdapter) throw new Error("Entity not found"); + const operationsTree = operationsTreeFactory.createQueryAST(resolveTree, entityAdapter, context); + debug(operationsTree.print()); + const clause = operationsTree.build(context, varName); + return clause.build(); } diff --git a/packages/graphql/src/translate/unwind-create.ts b/packages/graphql/src/translate/unwind-create.ts index 31b64b2260b..7127ab4892b 100644 --- a/packages/graphql/src/translate/unwind-create.ts +++ b/packages/graphql/src/translate/unwind-create.ts @@ -17,16 +17,20 @@ * limitations under the License. */ +import Cypher from "@neo4j/cypher-builder"; +import Debug from "debug"; import type { Node } from "../classes"; +import { CallbackBucket } from "../classes/CallbackBucket"; +import { DEBUG_TRANSLATE } from "../constants"; +import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; +import { getTreeDescriptor, mergeTreeDescriptors, parseCreate } from "./batch-create/parser"; import type { GraphQLCreateInput } from "./batch-create/types"; import { UnsupportedUnwindOptimization } from "./batch-create/types"; -import { mergeTreeDescriptors, getTreeDescriptor, parseCreate } from "./batch-create/parser"; import { UnwindCreateVisitor } from "./batch-create/unwind-create-visitors/UnwindCreateVisitor"; -import { CallbackBucket } from "../classes/CallbackBucket"; -import Cypher from "@neo4j/cypher-builder"; -import type { Neo4jGraphQLTranslationContext } from "../types/neo4j-graphql-translation-context"; -import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; import { QueryASTContext, QueryASTEnv } from "./queryAST/ast/QueryASTContext"; +import { QueryASTFactory } from "./queryAST/factory/QueryASTFactory"; + +const debug = Debug(DEBUG_TRANSLATE); export default async function unwindCreate({ context, @@ -61,6 +65,7 @@ export default async function unwindCreate({ concreteEntityAdapter, context ); + debug(queryAST.print()); const queryASTEnv = new QueryASTEnv(); const queryASTContext = new QueryASTContext({ target: rootNodeVariable, @@ -70,7 +75,6 @@ export default async function unwindCreate({ shouldCollect: true, }); const clauses = queryAST.transpile(queryASTContext).clauses; - const projectionCypher = clauses.length ? Cypher.concat(...clauses) : new Cypher.Return(new Cypher.Literal("Query cannot conclude with CALL")); diff --git a/packages/graphql/tests/e2e/subscriptions/authorization/filter/events.e2e.test.ts b/packages/graphql/tests/e2e/subscriptions/authorization/filter/events.e2e.test.ts new file mode 100644 index 00000000000..05a638fd912 --- /dev/null +++ b/packages/graphql/tests/e2e/subscriptions/authorization/filter/events.e2e.test.ts @@ -0,0 +1,159 @@ +/* + * 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 type { Response } from "supertest"; +import supertest from "supertest"; +import { Neo4jGraphQL } from "../../../../../src/classes"; +import { UniqueType } from "../../../../utils/graphql-types"; +import type { TestGraphQLServer } from "../../../setup/apollo-server"; +import { ApolloTestServer } from "../../../setup/apollo-server"; +import { WebSocketTestClient } from "../../../setup/ws-client"; +import Neo4j from "../../../setup/neo4j"; +import { createJwtHeader } from "../../../../utils/create-jwt-request"; +import { Neo4jGraphQLSubscriptionsDefaultEngine } from "../../../../../src/classes/subscription/Neo4jGraphQLSubscriptionsDefaultEngine"; + +describe("Subscriptions authorization with create events", () => { + let neo4j: Neo4j; + let driver: Driver; + let server: TestGraphQLServer; + let wsClient: WebSocketTestClient; + let User: UniqueType; + let key: string; + + beforeEach(async () => { + key = "secret"; + + User = new UniqueType("User"); + + const typeDefs = `#graphql + type JWTPayload @jwt { + roles: [String!]! @jwtClaim(path: "myApplication.roles") + } + + type ${User} + @subscriptionsAuthorization( + filter: [ + { events: [CREATED], where: { node: { id: "$jwt.sub" } } } + ] + ) { + id: ID! + } + `; + + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + + const neoSchema = new Neo4jGraphQL({ + typeDefs, + driver, + features: { + authorization: { key }, + subscriptions: new Neo4jGraphQLSubscriptionsDefaultEngine(), + }, + }); + + // eslint-disable-next-line @typescript-eslint/require-await + server = new ApolloTestServer(neoSchema, async ({ req }) => ({ + sessionConfig: { + database: neo4j.getIntegrationDatabaseName(), + }, + token: req.headers.authorization, + })); + await server.start(); + }); + + afterEach(async () => { + await wsClient.close(); + + await server.close(); + await driver.close(); + }); + + test("authorization filters don't apply to delete events", async () => { + const jwtToken = createJwtHeader(key, { sub: "user1", myApplication: { roles: ["user"] } }); + wsClient = new WebSocketTestClient(server.wsPath, jwtToken); + + await wsClient.subscribe(` + subscription { + ${User.operations.subscribe.deleted} { + ${User.operations.subscribe.payload.deleted} { + id + } + } + } + `); + + await createUser("user1"); + await createUser("user2"); + await deleteUser("user1"); + await deleteUser("user2"); + + await wsClient.waitForEvents(2); + + expect(wsClient.errors).toEqual([]); + expect(wsClient.events).toEqual([ + { + [User.operations.subscribe.deleted]: { + [User.operations.subscribe.payload.deleted]: { id: "user1" }, + }, + }, + { + [User.operations.subscribe.deleted]: { + [User.operations.subscribe.payload.deleted]: { id: "user2" }, + }, + }, + ]); + }); + + async function createUser(id: string): Promise { + const result = await supertest(server.path) + .post("") + .send({ + query: ` + mutation { + ${User.operations.create}(input: [{ id: "${id}" }]) { + ${User.plural} { + id + } + } + } + `, + }) + .expect(200); + return result; + } + + async function deleteUser(id: string): Promise { + const result = await supertest(server.path) + .post("") + .set("Authorization", createJwtHeader(key, { sub: "user1", myApplication: { roles: ["admin"] } })) + .send({ + query: ` + mutation { + ${User.operations.delete}(where: { id: "${id}" }) { + nodesDeleted + } + } + `, + }) + .expect(200); + return result; + } +}); diff --git a/packages/graphql/tests/integration/directives/fulltext/fulltext-query.int.test.ts b/packages/graphql/tests/integration/directives/fulltext/fulltext-query.int.test.ts index ea696b825cd..6a656340304 100644 --- a/packages/graphql/tests/integration/directives/fulltext/fulltext-query.int.test.ts +++ b/packages/graphql/tests/integration/directives/fulltext/fulltext-query.int.test.ts @@ -17,19 +17,19 @@ * limitations under the License. */ -import { gql } from "graphql-tag"; -import type { Driver, Session } from "neo4j-driver"; import type { GraphQLSchema } from "graphql"; import { graphql } from "graphql"; +import { gql } from "graphql-tag"; +import type { Driver, Session } from "neo4j-driver"; import { generate } from "randomstring"; -import Neo4j from "../../neo4j"; import { Neo4jGraphQL } from "../../../../src/classes"; -import { UniqueType } from "../../../utils/graphql-types"; +import { SCORE_FIELD } from "../../../../src/graphql/directives/fulltext"; import { upperFirst } from "../../../../src/utils/upper-first"; import { delay } from "../../../../src/utils/utils"; -import { isMultiDbUnsupportedError } from "../../../utils/is-multi-db-unsupported-error"; -import { SCORE_FIELD } from "../../../../src/graphql/directives/fulltext"; import { createBearerToken } from "../../../utils/create-bearer-token"; +import { UniqueType } from "../../../utils/graphql-types"; +import { isMultiDbUnsupportedError } from "../../../utils/is-multi-db-unsupported-error"; +import Neo4j from "../../neo4j"; function generatedTypeDefs(personType: UniqueType, movieType: UniqueType): string { return ` @@ -37,7 +37,7 @@ function generatedTypeDefs(personType: UniqueType, movieType: UniqueType): strin name: String! born: Int! actedInMovies: [${movieType.name}!]! @relationship(type: "ACTED_IN", direction: OUT) - } + } type ${movieType.name} { title: String! @@ -387,6 +387,7 @@ describe("@fulltext directive", () => { } } `; + const gqlResult = await graphql({ schema: generatedSchema, source: query, diff --git a/packages/graphql/tests/integration/experimental/interface-field-level-aggregations.int.test.ts b/packages/graphql/tests/integration/experimental/aggegations/aggregation-interfaces-field-level.int.test.ts similarity index 98% rename from packages/graphql/tests/integration/experimental/interface-field-level-aggregations.int.test.ts rename to packages/graphql/tests/integration/experimental/aggegations/aggregation-interfaces-field-level.int.test.ts index b219c71a067..c90a4cb82a5 100644 --- a/packages/graphql/tests/integration/experimental/interface-field-level-aggregations.int.test.ts +++ b/packages/graphql/tests/integration/experimental/aggegations/aggregation-interfaces-field-level.int.test.ts @@ -19,9 +19,9 @@ import { graphql } from "graphql"; import type { Driver, Session } from "neo4j-driver"; -import { Neo4jGraphQL } from "../../../src/classes"; -import { UniqueType } from "../../utils/graphql-types"; -import Neo4j from "../neo4j"; +import { Neo4jGraphQL } from "../../../../src/classes"; +import { UniqueType } from "../../../utils/graphql-types"; +import Neo4j from "../../neo4j"; describe("Interface Field Level Aggregations", () => { let driver: Driver; diff --git a/packages/graphql/tests/integration/experimental/aggregation-interfaces-top-level.int.test.ts b/packages/graphql/tests/integration/experimental/aggegations/aggregation-interfaces-top-level.int.test.ts similarity index 94% rename from packages/graphql/tests/integration/experimental/aggregation-interfaces-top-level.int.test.ts rename to packages/graphql/tests/integration/experimental/aggegations/aggregation-interfaces-top-level.int.test.ts index 4a87cd41458..d16ecd7f3bc 100644 --- a/packages/graphql/tests/integration/experimental/aggregation-interfaces-top-level.int.test.ts +++ b/packages/graphql/tests/integration/experimental/aggegations/aggregation-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 { 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"; +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("Top-level interface query fields", () => { const secret = "the-secret"; 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 new file mode 100644 index 00000000000..9fd2176c30c --- /dev/null +++ b/packages/graphql/tests/integration/experimental/aggegations/filter-aggregation-interfaces-field-level.int.test.ts @@ -0,0 +1,195 @@ +/* + * 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("Field-level filter interface query fields", () => { + const secret = "the-secret"; + + let schema: GraphQLSchema; + let neo4j: Neo4j; + let driver: Driver; + let typeDefs: string; + + const Production = new UniqueType("Production"); + const Movie = new UniqueType("Movie"); + const Actor = new UniqueType("Actor"); + const Series = new UniqueType("Series"); + + 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 */ ` + interface ${Production} { + title: String! + cost: Float! + } + + type ${Movie} implements ${Production} { + title: String! + cost: Float! + runtime: Int! + ${Actor.plural}: [${Actor}!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type ${Series} implements ${Production} { + title: String! + cost: Float! + episodes: Int! + } + + interface ActedIn @relationshipProperties { + screenTime: Int! + } + + type ${Actor} { + name: String! + actedIn: [${Production}!]! @relationship(type: "ACTED_IN", direction: OUT, properties: "ActedIn") + } + `; + + const session = await neo4j.getSession(); + + try { + await session.run(` + // Create Movies + CREATE (m1:${Movie} { title: "The Movie One", cost: 10000000, runtime: 120 }) + CREATE (m2:${Movie} { title: "The Movie Two", cost: 20000000, runtime: 90 }) + CREATE (m3:${Movie} { title: "The Movie Three", cost: 12000000, runtime: 70 }) + + // Create Series + CREATE (s1:${Series} { title: "The Series One", cost: 10000000, episodes: 10 }) + CREATE (s2:${Series} { title: "The Series Two", cost: 20000000, episodes: 20 }) + CREATE (s3:${Series} { title: "The Series Three", cost: 20000000, episodes: 15 }) + + // Create Actors + CREATE (a1:${Actor} { name: "Actor One" }) + CREATE (a2:${Actor} { name: "Actor Two" }) + + // Associate Actor 1 with Movies and Series + CREATE (a1)-[:ACTED_IN { screenTime: 100 }]->(m1) + CREATE (a1)-[:ACTED_IN { screenTime: 82 }]->(s1) + CREATE (a1)-[:ACTED_IN { screenTime: 20 }]->(m3) + CREATE (a1)-[:ACTED_IN { screenTime: 22 }]->(s3) + + // Associate Actor 2 with Movies and Series + CREATE (a2)-[:ACTED_IN { screenTime: 240 }]->(m2) + CREATE (a2)-[:ACTED_IN { screenTime: 728 }]->(s2) + CREATE (a2)-[:ACTED_IN { screenTime: 728 }]->(m3) + CREATE (a2)-[:ACTED_IN { screenTime: 88 }]->(s3) + `); + } 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]); + await session.close(); + await driver.close(); + }); + + test("complex query on nested aggregation", async () => { + const query = /* GraphQL */ ` + query { + ${Actor.plural} { + actedInAggregate(where: { title_STARTS_WITH: "The" }) { + 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 Series Three", + shortest: "The Movie One", + }, + }, + }, + name: "Actor One", + }, + { + actedInAggregate: { + edge: { + screenTime: { + max: 728, + min: 88, + }, + }, + node: { + title: { + longest: "The Series 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 new file mode 100644 index 00000000000..f284917224b --- /dev/null +++ b/packages/graphql/tests/integration/experimental/aggegations/filter-aggregation-interfaces-top-level.int.test.ts @@ -0,0 +1,145 @@ +/* + * 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("Top-level filter interface query fields", () => { + 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"); + + 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} implements Production { + title: String! + cost: Float! + runtime: Int + } + + type ${Series} implements Production { + title: String! + cost: Float! + episodes: Int + } + `; + + const session = await neo4j.getSession(); + + try { + await session.run(` + CREATE(m1:${Movie} {title: "A Movie", cost: 10}) + CREATE(m2:${Movie} {title: "The Matrix is a very interesting movie: The Documentary", cost: 20}) + + CREATE(s1:${Series} {title: "The Show", cost: 1}) + CREATE(s2:${Series} {title: "A Series 2", cost: 2}) + `); + } 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]); + await session.close(); + await driver.close(); + }); + + test("top level count", async () => { + const query = ` + query { + productionsAggregate(where: { title: "The Show" }) { + count + } + } + `; + + const token = createBearerToken(secret, {}); + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + productionsAggregate: { + count: 1, + }, + }); + }); + + test("top level count and string fields", async () => { + const query = ` + query { + productionsAggregate(where: { title_STARTS_WITH: "The" }) { + count + title { + longest + shortest + } + } + } + `; + + const token = createBearerToken(secret, {}); + const queryResult = await graphqlQuery(query, token); + expect(queryResult.errors).toBeUndefined(); + expect(queryResult.data).toEqual({ + productionsAggregate: { + count: 2, + title: { + longest: "The Matrix is a very interesting movie: The Documentary", + shortest: "The Show", + }, + }, + }); + }); +}); diff --git a/packages/graphql/tests/integration/issues/4268.int.test.ts b/packages/graphql/tests/integration/issues/4268.int.test.ts index 85db1ce1f6a..f6f932729d1 100644 --- a/packages/graphql/tests/integration/issues/4268.int.test.ts +++ b/packages/graphql/tests/integration/issues/4268.int.test.ts @@ -17,7 +17,7 @@ * limitations under the License. */ -import { type Driver } from "neo4j-driver"; +import type { Driver } from "neo4j-driver"; import Neo4j from "../neo4j"; import { Neo4jGraphQL } from "../../../src/classes"; import gql from "graphql-tag"; @@ -112,9 +112,10 @@ describe("https://github.com/neo4j/graphql/issues/4268", () => { const response = await graphql({ schema, source: query, - contextValue: neo4j.getContextValues({ jwt: { id: "some-id", email: "some-email", roles: ["not-an-admin"] } }), + contextValue: neo4j.getContextValues({ + jwt: { id: "some-id", email: "some-email", roles: ["not-an-admin"] }, + }), }); expect((response.errors as any[])[0].message).toBe("Forbidden"); - }); }); diff --git a/packages/graphql/tests/integration/issues/4292.int.test.ts b/packages/graphql/tests/integration/issues/4292.int.test.ts new file mode 100644 index 00000000000..08c4e392f94 --- /dev/null +++ b/packages/graphql/tests/integration/issues/4292.int.test.ts @@ -0,0 +1,277 @@ +/* + * 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/4292", () => { + let driver: Driver; + let neo4j: Neo4j; + let neo4jGraphql: Neo4jGraphQL; + const User = new UniqueType("User"); + const Group = new UniqueType("Group"); + const Person = new UniqueType("Person"); + const Admin = new UniqueType("Admin"); + const Contributor = new UniqueType("Contributor"); + + beforeAll(async () => { + neo4j = new Neo4j(); + driver = await neo4j.getDriver(); + const typeDefs = /* GraphQL */ ` + type JWT @jwt { + id: ID! + email: String! + roles: [String!]! + } + + type ${User.name} { + id: ID! @unique + email: String! @unique + name: String + creator: [${Group.name}!]! @relationship(type: "CREATOR_OF", direction: OUT) + admin: [${Admin.name}!]! @relationship(type: "IS_USER", direction: IN) + contributor: [${Contributor.name}!]! @relationship(type: "IS_USER", direction: IN) + invitations: [Invitee!]! @relationship(type: "CREATOR_OF", direction: OUT) + roles: [String!]! + } + + type ${Group.name} { + id: ID! @id @unique + name: String + members: [${Person.name}!]! @relationship(type: "MEMBER_OF", direction: IN) + creator: ${User.name}! + @relationship(type: "CREATOR_OF", direction: IN) + @settable(onCreate: true, onUpdate: true) + admins: [${Admin.name}!]! @relationship(type: "ADMIN_OF", direction: IN) + contributors: [${Contributor.name}!]! @relationship(type: "CONTRIBUTOR_TO", direction: IN) + } + + type ${Person.name} + @authorization( + validate: [ + { + operations: [CREATE] + where: { node: { group: { creator: { roles_INCLUDES: "plan:paid" } } } } + } + { + operations: [DELETE] + where: { + OR: [ + { node: { creator: { id: "$jwt.uid" } } } + { node: { group: { admins_SOME: { user: { id: "$jwt.uid" } } } } } + { node: { group: { creator: { id: "$jwt.uid" } } } } + ] + } + } + { + operations: [READ, UPDATE] + where: { + OR: [ + { node: { creator: { id: "$jwt.uid" } } } + { node: { group: { admins_SOME: { user: { id: "$jwt.uid" } } } } } + { node: { group: { contributors_SOME: { user: { id: "$jwt.uid" } } } } } + { node: { group: { creator: { id: "$jwt.uid" } } } } + ] + } + } + ] + ) { + id: ID! @id @unique + name: String! + creator: ${User.name}! + @relationship(type: "CREATOR_OF", direction: IN, nestedOperations: [CONNECT]) + @settable(onCreate: true, onUpdate: true) + group: ${Group.name}! @relationship(type: "MEMBER_OF", direction: OUT) + partners: [${Person.name}!]! + @relationship( + type: "PARTNER_OF" + queryDirection: UNDIRECTED_ONLY + direction: OUT + properties: "PartnerOf" + ) + } + + enum InviteeRole { + ADMIN + CONTRIBUTOR + } + + enum InviteeStatus { + INVITED + ACCEPTED + } + + interface Invitee { + id: ID! @id + email: String! + name: String + creator: ${User.name}! @relationship(type: "CREATOR_OF", direction: IN) + group: ${Group.name}! @relationship(type: "ADMIN_OF", direction: OUT) + status: InviteeStatus! @default(value: INVITED) + user: ${User.name} @relationship(type: "IS_USER", direction: OUT) + role: InviteeRole! + } + + type ${Admin.name} implements Invitee { + id: ID! @unique + group: ${Group.name}! + creator: ${User.name}! + email: String! + name: String + status: InviteeStatus! + user: ${User.name} + role: InviteeRole! @default(value: ADMIN) + } + + type ${Contributor.name} implements Invitee { + id: ID! @unique + group: ${Group.name}! @relationship(type: "CONTRIBUTOR_TO", direction: OUT) + creator: ${User.name}! + email: String! + name: String + status: InviteeStatus! + user: ${User.name} + role: InviteeRole! @default(value: CONTRIBUTOR) + } + + interface PartnerOf @relationshipProperties { + id: ID! @id + firstDay: Date + lastDay: Date + active: Boolean! @default(value: true) + } + + type JWT @jwt { + roles: [String!]! + } + + type Mutation { + sendInvite(id: ID!, role: InviteeRole!): Boolean! + } + + `; + neo4jGraphql = new Neo4jGraphQL({ + typeDefs, + driver, + features: { + authorization: { + key: "secret", + }, + }, + }); + + const session = await neo4j.getSession(); + try { + await session.run( + ` + CREATE (m:${Person.name} {title: "SomeTitle", id: "person-1", name: "SomePerson"})<-[:CREATOR_OF]-(u:${User.name} { id: "user-1", email: "email-1", roles: ["admin"]}) + CREATE (g:${Group.name} { id: "family_id_1", name: "group-1" })<-[:MEMBER_OF]-(m) + `, + {} + ); + } finally { + await session.close(); + } + }); + + afterAll(async () => { + const session = await neo4j.getSession(); + try { + await cleanNodes(session, [User.name, Group.name, Person.name, Admin.name, Contributor.name]); + } finally { + await session.close(); + } + await driver.close(); + }); + + test("should return groups with valid JWT", async () => { + const schema = await neo4jGraphql.getSchema(); + + const query = /* GraphQL */ ` + query Groups { + ${Group.plural}(where: { id: "family_id_1" }) { + id + name + members { + id + name + partnersConnection { + edges { + active + firstDay + lastDay + } + } + } + } + } + `; + + const response = await graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues({ jwt: { uid: "user-1", email: "some-email", roles: ["admin"] } }), + }); + expect(response.errors).toBeFalsy(); + expect(response.data?.[Group.plural]).toStrictEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: "family_id_1", + name: "group-1", + members: [expect.objectContaining({ id: "person-1", name: "SomePerson" })], + }), + ]) + ); + }); + + test("should raise Forbidden with invalid JWT", async () => { + const schema = await neo4jGraphql.getSchema(); + + const query = /* GraphQL */ ` + query Groups { + ${Group.plural}(where: { id: "family_id_1" }) { + id + name + members { + id + name + partnersConnection { + edges { + active + firstDay + lastDay + } + } + } + } + } + `; + + const response = await graphql({ + schema, + source: query, + contextValue: neo4j.getContextValues({ jwt: { uid: "not-user-1", email: "some-email", roles: ["admin"] } }), + }); + expect(response.errors?.[0]?.message).toContain("Forbidden"); + }); +}); diff --git a/packages/graphql/tests/integration/types/bigint.int.test.ts b/packages/graphql/tests/integration/types/bigint.int.test.ts index 184aaf9e6e2..b080cf83d6c 100644 --- a/packages/graphql/tests/integration/types/bigint.int.test.ts +++ b/packages/graphql/tests/integration/types/bigint.int.test.ts @@ -153,7 +153,7 @@ describe("BigInt", () => { }); test("should successfully query an node with a BigInt property using in where", async () => { - const session = driver.session(); + const session = await neo4j.getSession(); const File = new UniqueType("File"); const typeDefs = ` @@ -190,7 +190,7 @@ describe("BigInt", () => { const gqlResult = await graphql({ schema: await neoSchema.getSchema(), source: query, - contextValue: { executionContext: driver, sessionConfig: { bookmarks: session.lastBookmark() } }, + contextValue: neo4j.getContextValues(), }); expect(gqlResult.errors).toBeFalsy(); diff --git a/packages/graphql/tests/tck/aggregations/field-level-aggregations/field-level-aggregations-edge.test.ts b/packages/graphql/tests/tck/aggregations/field-level-aggregations/field-level-aggregations-edge.test.ts index d3f5df051a6..05d084b1b77 100644 --- a/packages/graphql/tests/tck/aggregations/field-level-aggregations/field-level-aggregations-edge.test.ts +++ b/packages/graphql/tests/tck/aggregations/field-level-aggregations/field-level-aggregations-edge.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("Field Level Aggregations", () => { let typeDefs: DocumentNode; diff --git a/packages/graphql/tests/tck/aggregations/field-level-aggregations/field-level-aggregations.test.ts b/packages/graphql/tests/tck/aggregations/field-level-aggregations/field-level-aggregations.test.ts index 22ed931d3b4..dc19a889fd7 100644 --- a/packages/graphql/tests/tck/aggregations/field-level-aggregations/field-level-aggregations.test.ts +++ b/packages/graphql/tests/tck/aggregations/field-level-aggregations/field-level-aggregations.test.ts @@ -101,13 +101,13 @@ describe("Field Level Aggregations", () => { } CALL { WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - WITH this1 - ORDER BY size(this1.name) DESC - WITH collect(this1.name) AS list - RETURN { longest: head(list), shortest: last(list) } AS var3 + MATCH (this)<-[this3:ACTED_IN]-(this4:Actor) + WITH this4 + ORDER BY size(this4.name) DESC + WITH collect(this4.name) AS list + RETURN { longest: head(list), shortest: last(list) } AS var5 } - RETURN this { actorsAggregate: { count: var2, node: { name: var3 } } } AS this" + RETURN this { actorsAggregate: { count: var2, node: { name: var5 } } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -247,10 +247,10 @@ describe("Field Level Aggregations", () => { } CALL { WITH this - MATCH (this)<-[this0:ACTED_IN]-(this1:Actor) - RETURN { min: min(this1.age), max: max(this1.age), average: avg(this1.age), sum: sum(this1.age) } AS var3 + MATCH (this)<-[this3:ACTED_IN]-(this4:Actor) + RETURN { min: min(this4.age), max: max(this4.age), average: avg(this4.age), sum: sum(this4.age) } AS var5 } - RETURN this { actorsAggregate: { node: { name: var2, age: var3 } } } AS this" + RETURN this { actorsAggregate: { node: { name: var2, age: var5 } } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); diff --git a/packages/graphql/tests/tck/aggregations/many.test.ts b/packages/graphql/tests/tck/aggregations/many.test.ts index fdecca9e2f4..85b1428a853 100644 --- a/packages/graphql/tests/tck/aggregations/many.test.ts +++ b/packages/graphql/tests/tck/aggregations/many.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("Cypher Aggregations Many", () => { let typeDefs: DocumentNode; 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 5dcd054497d..a7b01afb791 100644 --- a/packages/graphql/tests/tck/connections/relationship_properties/create.test.ts +++ b/packages/graphql/tests/tck/connections/relationship_properties/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("Relationship Properties Create Cypher", () => { let typeDefs: DocumentNode; diff --git a/packages/graphql/tests/tck/directives/plural.test.ts b/packages/graphql/tests/tck/directives/plural.test.ts index 95bb0bbfaf9..e7fd9779b5a 100644 --- a/packages/graphql/tests/tck/directives/plural.test.ts +++ b/packages/graphql/tests/tck/directives/plural.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("Plural directive", () => { let typeDefs: DocumentNode; diff --git a/packages/graphql/tests/tck/experimental/interface-field-level-aggregations.test.ts b/packages/graphql/tests/tck/experimental/aggregations/aggregation-interfaces-field-level.test.ts similarity index 70% rename from packages/graphql/tests/tck/experimental/interface-field-level-aggregations.test.ts rename to packages/graphql/tests/tck/experimental/aggregations/aggregation-interfaces-field-level.test.ts index 94a8fabd5da..51524478a77 100644 --- a/packages/graphql/tests/tck/experimental/interface-field-level-aggregations.test.ts +++ b/packages/graphql/tests/tck/experimental/aggregations/aggregation-interfaces-field-level.test.ts @@ -19,8 +19,8 @@ import type { DocumentNode } from "graphql"; import { gql } from "graphql-tag"; -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; +import { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../utils/tck-test-utils"; describe("Interface Field Level Aggregations", () => { let typeDefs: DocumentNode; @@ -82,15 +82,15 @@ describe("Interface Field Level Aggregations", () => { CALL { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - RETURN this1 AS var2 + RETURN this1 AS node, this0 AS edge UNION WITH this - MATCH (this)-[this3:ACTED_IN]->(this4:Series) - RETURN this4 AS var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Series) + RETURN this3 AS node, this2 AS edge } - RETURN count(var2) AS var2 + RETURN count(node) AS this4 } - RETURN this { actedInAggregate: { count: var2 } } AS this" + RETURN this { actedInAggregate: { count: this4 } } AS this" `); }); @@ -116,15 +116,15 @@ describe("Interface Field Level Aggregations", () => { CALL { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - RETURN this1 AS var2 + RETURN this1 AS node, this0 AS edge UNION WITH this - MATCH (this)-[this3:ACTED_IN]->(this4:Series) - RETURN this4 AS var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Series) + RETURN this3 AS node, this2 AS edge } - RETURN count(var2) AS var2 + RETURN count(node) AS this4 } - RETURN this { actedInAggregate: { count: var2 } } AS this" + RETURN this { actedInAggregate: { count: this4 } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -158,15 +158,15 @@ describe("Interface Field Level Aggregations", () => { CALL { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - RETURN this1 AS var2 + RETURN this1 AS node, this0 AS edge UNION WITH this - MATCH (this)-[this3:ACTED_IN]->(this4:Series) - RETURN this4 AS var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Series) + RETURN this3 AS node, this2 AS edge } - RETURN { min: min(var2.cost) } AS var2 + RETURN { min: min(node.cost) } AS this4 } - RETURN this { actedInAggregate: { node: { cost: var2 } } } AS this" + RETURN this { actedInAggregate: { node: { cost: this4 } } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -196,15 +196,15 @@ describe("Interface Field Level Aggregations", () => { CALL { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - RETURN this1 AS var2 + RETURN this1 AS node, this0 AS edge UNION WITH this - MATCH (this)-[this3:ACTED_IN]->(this4:Series) - RETURN this4 AS var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Series) + RETURN this3 AS node, this2 AS edge } - RETURN { max: max(var2.cost) } AS var2 + RETURN { max: max(node.cost) } AS this4 } - RETURN this { actedInAggregate: { node: { cost: var2 } } } AS this" + RETURN this { actedInAggregate: { node: { cost: this4 } } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -229,35 +229,35 @@ describe("Interface Field Level Aggregations", () => { 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 var2 - UNION - WITH this - MATCH (this)-[this3:ACTED_IN]->(this4:Series) - RETURN this4 AS var2 - } - RETURN count(var2) AS var2 - } - CALL { - WITH this - CALL { - WITH this - MATCH (this)-[this5:ACTED_IN]->(this6:Movie) - RETURN this6 AS var7 - UNION - WITH this - MATCH (this)-[this8:ACTED_IN]->(this9:Series) - RETURN this9 AS var7 - } - RETURN { max: max(var7.cost) } AS var7 - } - RETURN this { actedInAggregate: { count: var2, node: { cost: var7 } } } AS this" - `); + "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 + } + 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 + } + RETURN { max: max(node.cost) } AS this9 + } + RETURN this { actedInAggregate: { count: this4, node: { cost: this9 } } } AS this" + `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); }); @@ -286,18 +286,18 @@ describe("Interface Field Level Aggregations", () => { CALL { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - RETURN this1 AS var2 + RETURN this1 AS node, this0 AS edge UNION WITH this - MATCH (this)-[this3:ACTED_IN]->(this4:Series) - RETURN this4 AS var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Series) + RETURN this3 AS node, this2 AS edge } - WITH var2 - ORDER BY size(var2.title) DESC - WITH collect(var2.title) AS list - RETURN { longest: head(list) } AS var2 + WITH node + ORDER BY size(node.title) DESC + WITH collect(node.title) AS list + RETURN { longest: head(list) } AS this4 } - RETURN this { actedInAggregate: { node: { title: var2 } } } AS this" + RETURN this { actedInAggregate: { node: { title: this4 } } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -333,18 +333,18 @@ describe("Interface Field Level Aggregations", () => { CALL { WITH this1 MATCH (this1)-[this2:ACTED_IN]->(this3:Movie) - RETURN this3 AS var4 + RETURN this3 AS node, this2 AS edge UNION WITH this1 - MATCH (this1)-[this5:ACTED_IN]->(this6:Series) - RETURN this6 AS var4 + MATCH (this1)-[this4:ACTED_IN]->(this5:Series) + RETURN this5 AS node, this4 AS edge } - WITH var4 - ORDER BY size(var4.title) DESC - WITH collect(var4.title) AS list - RETURN { longest: head(list) } AS var4 + WITH node + ORDER BY size(node.title) DESC + WITH collect(node.title) AS list + RETURN { longest: head(list) } AS this6 } - WITH this1 { actedInAggregate: { node: { title: var4 } } } AS this1 + WITH this1 { actedInAggregate: { node: { title: this6 } } } AS this1 RETURN collect(this1) AS var7 } RETURN this { actors: var7 } AS this" @@ -377,15 +377,15 @@ describe("Interface Field Level Aggregations", () => { CALL { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - RETURN this0 AS var2 + RETURN this1 AS node, this0 AS edge UNION WITH this - MATCH (this)-[this3:ACTED_IN]->(this4:Series) - RETURN this3 AS var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Series) + RETURN this3 AS node, this2 AS edge } - RETURN { sum: sum(var2.screenTime) } AS var2 + RETURN { sum: sum(edge.screenTime) } AS this4 } - RETURN this { actedInAggregate: { edge: { screenTime: var2 } } } AS this" + RETURN this { actedInAggregate: { edge: { screenTime: this4 } } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -421,41 +421,41 @@ describe("Interface Field Level Aggregations", () => { CALL { WITH this MATCH (this)-[this0:ACTED_IN]->(this1:Movie) - RETURN this1 AS var2 + RETURN this1 AS node, this0 AS edge UNION WITH this - MATCH (this)-[this3:ACTED_IN]->(this4:Series) - RETURN this4 AS var2 + MATCH (this)-[this2:ACTED_IN]->(this3:Series) + RETURN this3 AS node, this2 AS edge } - RETURN count(var2) AS var2 + RETURN count(node) AS this4 } CALL { WITH this CALL { WITH this MATCH (this)-[this5:ACTED_IN]->(this6:Movie) - RETURN this6 AS var7 + RETURN this6 AS node, this5 AS edge UNION WITH this - MATCH (this)-[this8:ACTED_IN]->(this9:Series) - RETURN this9 AS var7 + MATCH (this)-[this7:ACTED_IN]->(this8:Series) + RETURN this8 AS node, this7 AS edge } - RETURN { sum: sum(var7.cost) } AS var7 + RETURN { sum: sum(node.cost) } AS this9 } CALL { WITH this CALL { WITH this MATCH (this)-[this10:ACTED_IN]->(this11:Movie) - RETURN this10 AS var12 + RETURN this11 AS node, this10 AS edge UNION WITH this - MATCH (this)-[this13:ACTED_IN]->(this14:Series) - RETURN this13 AS var12 + MATCH (this)-[this12:ACTED_IN]->(this13:Series) + RETURN this13 AS node, this12 AS edge } - RETURN { sum: sum(var12.screenTime) } AS var12 + RETURN { sum: sum(edge.screenTime) } AS this14 } - RETURN this { actedInAggregate: { count: var2, node: { cost: var7 }, edge: { screenTime: var12 } } } AS this" + RETURN this { actedInAggregate: { count: this4, node: { cost: this9 }, edge: { screenTime: this14 } } } AS this" `); }); }); diff --git a/packages/graphql/tests/tck/experimental/aggregation-top-level-interface.test.ts b/packages/graphql/tests/tck/experimental/aggregations/aggregation-interfaces-top-level.test.ts similarity index 79% rename from packages/graphql/tests/tck/experimental/aggregation-top-level-interface.test.ts rename to packages/graphql/tests/tck/experimental/aggregations/aggregation-interfaces-top-level.test.ts index 37f50797106..c6e4cba3d49 100644 --- a/packages/graphql/tests/tck/experimental/aggregation-top-level-interface.test.ts +++ b/packages/graphql/tests/tck/experimental/aggregations/aggregation-interfaces-top-level.test.ts @@ -19,8 +19,8 @@ import type { DocumentNode } from "graphql"; import { gql } from "graphql-tag"; -import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; +import { Neo4jGraphQL } from "../../../../src"; +import { formatCypher, formatParams, translateQuery } from "../../utils/tck-test-utils"; describe("Top level aggregation interfaces", () => { let typeDefs: DocumentNode; @@ -76,14 +76,14 @@ describe("Top level aggregation interfaces", () => { "CALL { CALL { MATCH (this0:Movie) - RETURN this0 AS var1 + RETURN this0 AS node UNION - MATCH (this2:Series) - RETURN this2 AS var1 + MATCH (this1:Series) + RETURN this1 AS node } - RETURN count(var1) AS var1 + RETURN count(node) AS this2 } - RETURN { count: var1 }" + RETURN { count: this2 }" `); expect(formatParams(result.params)).toMatchInlineSnapshot(`"{}"`); @@ -108,27 +108,27 @@ describe("Top level aggregation interfaces", () => { "CALL { CALL { MATCH (this0:Movie) - RETURN this0 AS var1 + RETURN this0 AS node UNION - MATCH (this2:Series) - RETURN this2 AS var1 + MATCH (this1:Series) + RETURN this1 AS node } - RETURN count(var1) AS var1 + RETURN count(node) AS this2 } CALL { CALL { MATCH (this3:Movie) - RETURN this3 AS var4 + RETURN this3 AS node UNION - MATCH (this5:Series) - RETURN this5 AS var4 + MATCH (this4:Series) + RETURN this4 AS node } - WITH var4 - ORDER BY size(var4.title) DESC - WITH collect(var4.title) AS list - RETURN { longest: head(list), shortest: last(list) } AS var4 + WITH node + ORDER BY size(node.title) DESC + WITH collect(node.title) AS list + RETURN { longest: head(list), shortest: last(list) } AS this5 } - RETURN { count: var1, title: var4 }" + RETURN { count: this2, title: this5 }" `); 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 new file mode 100644 index 00000000000..a541177ae35 --- /dev/null +++ b/packages/graphql/tests/tck/experimental/aggregations/filter-aggregation-interfaces-field-level.test.ts @@ -0,0 +1,192 @@ +/* + * 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 { formatCypher, formatParams, translateQuery } from "../../utils/tck-test-utils"; + +describe("Interface Field Level Aggregations", () => { + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = gql` + interface Production { + title: String! + cost: Float! + } + + type Movie implements Production { + title: String! + cost: Float! + runtime: Int! + actors: [Actor!]! @relationship(type: "ACTED_IN", direction: IN, properties: "ActedIn") + } + + type Series implements Production { + title: String! + cost: Float! + episodes: Int! + } + + 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("Count with where", async () => { + const query = gql` + { + actors { + actedInAggregate(where: { title: "The Matrix" }) { + 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 + } + WITH * + WHERE node.title = $param0 + RETURN count(node) AS this4 + } + RETURN this { actedInAggregate: { count: this4 } } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\" + }" + `); + }); + + test("Count with where and string aggregation", async () => { + const query = gql` + { + actors { + actedInAggregate(where: { title_STARTS_WITH: "The" }) { + 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 + 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 $param1 + 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 $param2 + 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\\": \\"The\\", + \\"param2\\": \\"The\\" + }" + `); + }); +}); 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 new file mode 100644 index 00000000000..94f7b43f175 --- /dev/null +++ b/packages/graphql/tests/tck/experimental/aggregations/filter-aggregation-interfaces-top-level.test.ts @@ -0,0 +1,131 @@ +/* + * 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 { formatCypher, formatParams, translateQuery } from "../../utils/tck-test-utils"; + +describe("Top level filter on aggregation interfaces", () => { + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + 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! + } + + 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 count", async () => { + const query = gql` + { + productionsAggregate(where: { 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.title = $param0 + RETURN count(node) AS this2 + } + RETURN { count: this2 }" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\" + }" + `); + }); + + test("top level count and string fields", async () => { + const query = gql` + { + productionsAggregate(where: { 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.title = $param0 + RETURN count(node) AS this2 + } + RETURN { count: this2 }" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"The Matrix\\" + }" + `); + }); +}); 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 857684ffff7..2bd74aa1393 100644 --- a/packages/graphql/tests/tck/experimental/union-top-level.test.ts +++ b/packages/graphql/tests/tck/experimental/union-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("Union top level operations", () => { const secret = "secret"; diff --git a/packages/graphql/tests/tck/fulltext/aggregate.test.ts b/packages/graphql/tests/tck/fulltext/aggregate.test.ts index 531cc5d2a7b..52cd317823e 100644 --- a/packages/graphql/tests/tck/fulltext/aggregate.test.ts +++ b/packages/graphql/tests/tck/fulltext/aggregate.test.ts @@ -50,9 +50,12 @@ describe("Cypher -> fulltext -> Aggregate", () => { const result = await translateQuery(neoSchema, query, {}); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this - WHERE $param1 IN labels(this) - RETURN { count: count(this) }" + "CALL { + CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) + RETURN count(this0) AS var2 + } + RETURN { count: var2 }" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/fulltext/auth.test.ts b/packages/graphql/tests/tck/fulltext/auth.test.ts index 80f2def26e4..2a76ee0ea17 100644 --- a/packages/graphql/tests/tck/fulltext/auth.test.ts +++ b/packages/graphql/tests/tck/fulltext/auth.test.ts @@ -19,8 +19,8 @@ import { gql } from "graphql-tag"; import { Neo4jGraphQL } from "../../../src"; -import { formatCypher, translateQuery } from "../utils/tck-test-utils"; import { createBearerToken } from "../../utils/create-bearer-token"; +import { formatCypher, translateQuery } from "../utils/tck-test-utils"; describe("Cypher -> fulltext -> Auth", () => { let verifyTCK; @@ -75,10 +75,11 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "4.4" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND ($isAuthenticated = true AND size([(this)<-[:DIRECTED]-(this0:Person) WHERE ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) | 1]) > 0)) - RETURN this { .title } AS this" + WHERE ($isAuthenticated = true AND size([(this0)<-[:DIRECTED]-(this2:Person) WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) | 1]) > 0) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -130,10 +131,11 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "4.4" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this)<-[:DIRECTED]-(this0:Person) WHERE ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[:DIRECTED]-(this2:Person) WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -187,10 +189,11 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "4.4" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this)<-[:DIRECTED]-(this0:Person) WHERE NOT ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) | 1]) = 0), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[:DIRECTED]-(this2:Person) WHERE NOT ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) | 1]) = 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -246,10 +249,11 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "4.4" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this)<-[this1:DIRECTED]-(this0:Person) WHERE ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[this3:DIRECTED]-(this2:Person) WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -308,10 +312,11 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "4.4" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this)<-[this1:DIRECTED]-(this0:Person) WHERE NOT ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) | 1]) = 0), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[this3:DIRECTED]-(this2:Person) WHERE NOT ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) | 1]) = 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -371,10 +376,11 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "4.4" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this)<-[this0:DIRECTED]-(this1:Person) WHERE ($param3 IS NOT NULL AND this0.year = $param3) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[this2:DIRECTED]-(this3:Person) WHERE ($param3 IS NOT NULL AND this2.year = $param3) | 1]) > 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -431,10 +437,11 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "4.4" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this)<-[this0:DIRECTED]-(this1:Person) WHERE NOT ($param3 IS NOT NULL AND this0.year = $param3) | 1]) = 0), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND size([(this0)<-[this2:DIRECTED]-(this3:Person) WHERE NOT ($param3 IS NOT NULL AND this2.year = $param3) | 1]) = 0), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -485,13 +492,14 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "5" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:DIRECTED]-(this0:Person) - WHERE ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) - })) - RETURN this { .title } AS this" + WHERE ($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[:DIRECTED]-(this2:Person) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + }) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -543,13 +551,14 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "5" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[:DIRECTED]-(this0:Person) - WHERE ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[:DIRECTED]-(this2:Person) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -603,16 +612,17 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "5" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (EXISTS { - MATCH (this)<-[:DIRECTED]-(this0:Person) - WHERE ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (EXISTS { + MATCH (this0)<-[:DIRECTED]-(this2:Person) + WHERE ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) } AND NOT (EXISTS { - MATCH (this)<-[:DIRECTED]-(this0:Person) - WHERE NOT ($jwt.sub IS NOT NULL AND this0.id = $jwt.sub) - }))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + MATCH (this0)<-[:DIRECTED]-(this2:Person) + WHERE NOT ($jwt.sub IS NOT NULL AND this2.id = $jwt.sub) + }))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -668,13 +678,14 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "5" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[this0:DIRECTED]-(this1:Person) - WHERE ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[this2:DIRECTED]-(this3:Person) + WHERE ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -733,16 +744,17 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "5" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (EXISTS { - MATCH (this)<-[this0:DIRECTED]-(this1:Person) - WHERE ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (EXISTS { + MATCH (this0)<-[this2:DIRECTED]-(this3:Person) + WHERE ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) } AND NOT (EXISTS { - MATCH (this)<-[this0:DIRECTED]-(this1:Person) - WHERE NOT ($jwt.sub IS NOT NULL AND this1.id = $jwt.sub) - }))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + MATCH (this0)<-[this2:DIRECTED]-(this3:Person) + WHERE NOT ($jwt.sub IS NOT NULL AND this3.id = $jwt.sub) + }))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -802,13 +814,14 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "5" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { - MATCH (this)<-[this0:DIRECTED]-(this1:Person) - WHERE ($param3 IS NOT NULL AND this0.year = $param3) - }), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND EXISTS { + MATCH (this0)<-[this2:DIRECTED]-(this3:Person) + WHERE ($param3 IS NOT NULL AND this2.year = $param3) + }), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` @@ -865,16 +878,17 @@ describe("Cypher -> fulltext -> Auth", () => { const result = await translateQuery(neoSchema, query, { token, neo4jVersion: "5" }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) WITH * - WHERE ($param1 IN labels(this) AND apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (EXISTS { - MATCH (this)<-[this0:DIRECTED]-(this1:Person) - WHERE ($param3 IS NOT NULL AND this0.year = $param3) + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND (EXISTS { + MATCH (this0)<-[this2:DIRECTED]-(this3:Person) + WHERE ($param3 IS NOT NULL AND this2.year = $param3) } AND NOT (EXISTS { - MATCH (this)<-[this0:DIRECTED]-(this1:Person) - WHERE NOT ($param3 IS NOT NULL AND this0.year = $param3) - }))), \\"@neo4j/graphql/FORBIDDEN\\", [0])) - RETURN this { .title } AS this" + MATCH (this0)<-[this2:DIRECTED]-(this3:Person) + WHERE NOT ($param3 IS NOT NULL AND this2.year = $param3) + }))), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + RETURN this0 { .title } AS this" `); expect(result.params).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/fulltext/match.test.ts b/packages/graphql/tests/tck/fulltext/match.test.ts index e81ea669503..10050ac53a8 100644 --- a/packages/graphql/tests/tck/fulltext/match.test.ts +++ b/packages/graphql/tests/tck/fulltext/match.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("Cypher -> fulltext -> Match", () => { let typeDefs: DocumentNode; @@ -50,9 +50,9 @@ describe("Cypher -> fulltext -> Match", () => { const result = await translateQuery(neoSchema, query, {}); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this - WHERE $param1 IN labels(this) - RETURN this { .title } AS this" + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) + RETURN this0 { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -78,16 +78,16 @@ describe("Cypher -> fulltext -> Match", () => { const result = await translateQuery(neoSchema, query, {}); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this - WHERE (this.title = $param1 AND $param2 IN labels(this)) - RETURN this { .title } AS this" + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE ($param1 IN labels(this0) AND this0.title = $param2) + RETURN this0 { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ \\"param0\\": \\"something AND something\\", - \\"param1\\": \\"some-title\\", - \\"param2\\": \\"Movie\\" + \\"param1\\": \\"Movie\\", + \\"param2\\": \\"some-title\\" }" `); }); diff --git a/packages/graphql/tests/tck/fulltext/node-labels.test.ts b/packages/graphql/tests/tck/fulltext/node-labels.test.ts index 69bacf2938f..75fe3842a78 100644 --- a/packages/graphql/tests/tck/fulltext/node-labels.test.ts +++ b/packages/graphql/tests/tck/fulltext/node-labels.test.ts @@ -19,8 +19,8 @@ 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 -> fulltext -> Additional Labels", () => { test("simple match with single fulltext property and static additionalLabels", async () => { @@ -47,9 +47,9 @@ describe("Cypher -> fulltext -> Additional Labels", () => { const result = await translateQuery(neoSchema, query); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this - WHERE ($param1 IN labels(this) AND $param2 IN labels(this)) - RETURN this { .title } AS this" + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE ($param1 IN labels(this0) AND $param2 IN labels(this0)) + RETURN this0 { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -93,9 +93,9 @@ describe("Cypher -> fulltext -> Additional Labels", () => { }); expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` - "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this - WHERE ($param1 IN labels(this) AND $param2 IN labels(this)) - RETURN this { .title } AS this" + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE ($param1 IN labels(this0) AND $param2 IN labels(this0)) + RETURN this0 { .title } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` diff --git a/packages/graphql/tests/tck/fulltext/score.test.ts b/packages/graphql/tests/tck/fulltext/score.test.ts new file mode 100644 index 00000000000..b92f00598d7 --- /dev/null +++ b/packages/graphql/tests/tck/fulltext/score.test.ts @@ -0,0 +1,225 @@ +/* + * 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 { formatCypher, formatParams, translateQuery } from "../utils/tck-test-utils"; + +describe("Cypher -> fulltext -> Score", () => { + let typeDefs: DocumentNode; + let neoSchema: Neo4jGraphQL; + + beforeAll(() => { + typeDefs = gql` + type Movie @fulltext(indexes: [{ name: "MovieTitle", fields: ["title"] }]) { + title: String + released: Int + } + `; + + neoSchema = new Neo4jGraphQL({ + typeDefs, + }); + }); + + test("simple match with single property and score", async () => { + const query = gql` + query { + moviesFulltextMovieTitle(phrase: "a different name") { + score + movie { + title + released + } + } + } + `; + + const result = await translateQuery(neoSchema, query, {}); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) + RETURN this0 { .title, .released } AS movie, var1 AS score" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"a different name\\", + \\"param1\\": \\"Movie\\" + }" + `); + }); + + test("simple match with single property and score and filter", async () => { + const query = gql` + query { + moviesFulltextMovieTitle(phrase: "a different name", where: { movie: { released_GT: 2000 } }) { + score + movie { + title + released + } + } + } + `; + + const result = await translateQuery(neoSchema, query, {}); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE ($param1 IN labels(this0) AND this0.released > $param2) + RETURN this0 { .title, .released } AS movie, var1 AS score" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"a different name\\", + \\"param1\\": \\"Movie\\", + \\"param2\\": { + \\"low\\": 2000, + \\"high\\": 0 + } + }" + `); + }); + + test("with score filtering", async () => { + const query = gql` + query { + moviesFulltextMovieTitle(phrase: "a different name", where: { score: { min: 0.5 } }) { + score + movie { + title + } + } + } + `; + + const result = await translateQuery(neoSchema, query, {}); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE ($param1 IN labels(this0) AND var1 >= $param2) + RETURN this0 { .title } AS movie, var1 AS score" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"a different name\\", + \\"param1\\": \\"Movie\\", + \\"param2\\": 0.5 + }" + `); + }); + + test("with sorting", async () => { + const query = gql` + query { + moviesFulltextMovieTitle(phrase: "a different name", sort: { movie: { title: DESC } }) { + score + movie { + title + } + } + } + `; + + const result = await translateQuery(neoSchema, query, {}); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) + WITH * + ORDER BY this0.title DESC + RETURN this0 { .title } AS movie, var1 AS score" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"a different name\\", + \\"param1\\": \\"Movie\\" + }" + `); + }); + + test("with score sorting", async () => { + const query = gql` + query { + moviesFulltextMovieTitle(phrase: "a different name", sort: { score: ASC }) { + score + movie { + title + } + } + } + `; + + const result = await translateQuery(neoSchema, query, {}); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) + WITH * + ORDER BY var1 ASC + RETURN this0 { .title } AS movie, var1 AS score" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"a different name\\", + \\"param1\\": \\"Movie\\" + }" + `); + }); + + test("with score and normal sorting", async () => { + const query = gql` + query { + moviesFulltextMovieTitle( + phrase: "a different name" + sort: [{ score: ASC }, { movie: { title: DESC } }] + ) { + score + movie { + title + } + } + } + `; + + const result = await translateQuery(neoSchema, query, {}); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "CALL db.index.fulltext.queryNodes(\\"MovieTitle\\", $param0) YIELD node AS this0, score AS var1 + WHERE $param1 IN labels(this0) + WITH * + ORDER BY var1 ASC, this0.title DESC + RETURN this0 { .title } AS movie, var1 AS score" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"a different name\\", + \\"param1\\": \\"Movie\\" + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/issues/1933.test.ts b/packages/graphql/tests/tck/issues/1933.test.ts index f6427b805b7..fd4a2611139 100644 --- a/packages/graphql/tests/tck/issues/1933.test.ts +++ b/packages/graphql/tests/tck/issues/1933.test.ts @@ -94,10 +94,10 @@ describe("https://github.com/neo4j/graphql/issues/1933", () => { } CALL { WITH this - MATCH (this)-[this3:PARTICIPATES]->(this4:Project) - RETURN { min: min(this3.allocation), max: max(this3.allocation), average: avg(this3.allocation), sum: sum(this3.allocation) } AS var6 + MATCH (this)-[this6:PARTICIPATES]->(this7:Project) + RETURN { min: min(this6.allocation), max: max(this6.allocation), average: avg(this6.allocation), sum: sum(this6.allocation) } AS var8 } - RETURN this { .employeeId, .firstName, .lastName, projectsAggregate: { count: var5, edge: { allocation: var6 } } } AS this" + RETURN this { .employeeId, .firstName, .lastName, projectsAggregate: { count: var5, edge: { allocation: var8 } } } AS this" `); expect(formatParams(result.params)).toMatchInlineSnapshot(` @@ -147,10 +147,10 @@ describe("https://github.com/neo4j/graphql/issues/1933", () => { } CALL { WITH this - MATCH (this)-[this4:PARTICIPATES]->(this5:Project) - RETURN { min: min(this4.allocation), max: max(this4.allocation), average: avg(this4.allocation), sum: sum(this4.allocation) } AS var7 + MATCH (this)-[this7:PARTICIPATES]->(this8:Project) + RETURN { min: min(this7.allocation), max: max(this7.allocation), average: avg(this7.allocation), sum: sum(this7.allocation) } AS var9 } - RETURN this { .employeeId, .firstName, .lastName, projectsAggregate: { count: var6, edge: { allocation: var7 } } } AS this" + RETURN this { .employeeId, .firstName, .lastName, projectsAggregate: { count: var6, edge: { allocation: var9 } } } 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 b75774a250b..db800738d35 100644 --- a/packages/graphql/tests/tck/issues/4118.test.ts +++ b/packages/graphql/tests/tck/issues/4118.test.ts @@ -223,7 +223,8 @@ describe("https://github.com/neo4j/graphql/issues/2871", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"isAuthenticated\\": true, + \\"isAuthenticated\\": false, + \\"jwt\\": {}, \\"create_param2\\": \\"overlord\\", \\"this0_host_connect0_node_param0\\": \\"userid\\", \\"authorization_0_0_0_0_before_param2\\": \\"overlord\\", diff --git a/packages/graphql/tests/tck/issues/4170.test.ts b/packages/graphql/tests/tck/issues/4170.test.ts index f1185230c88..1351b88f20b 100644 --- a/packages/graphql/tests/tck/issues/4170.test.ts +++ b/packages/graphql/tests/tck/issues/4170.test.ts @@ -227,7 +227,8 @@ describe("https://github.com/neo4j/graphql/issues/4170", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"isAuthenticated\\": true, + \\"isAuthenticated\\": false, + \\"jwt\\": {}, \\"this0_settings0_node_openingDays0_node_open0_node_name\\": \\"lambo\\", \\"this0_admins0_node_userId\\": \\"123\\", \\"resolvedCallbacks\\": { diff --git a/packages/graphql/tests/tck/issues/4223.test.ts b/packages/graphql/tests/tck/issues/4223.test.ts index b0205c522ed..babf4cfc3b2 100644 --- a/packages/graphql/tests/tck/issues/4223.test.ts +++ b/packages/graphql/tests/tck/issues/4223.test.ts @@ -273,7 +273,8 @@ describe("https://github.com/neo4j/graphql/issues/4223", () => { expect(formatParams(result.params)).toMatchInlineSnapshot(` "{ - \\"isAuthenticated\\": true, + \\"isAuthenticated\\": false, + \\"jwt\\": {}, \\"this0_settings0_node_openingDays0_node_open0_node_name\\": \\"lambo\\", \\"this0_settings0_node_myWorkspace0_node_workspace\\": \\"myWorkspace\\", \\"this0_admins0_node_userId\\": \\"123\\", diff --git a/packages/graphql/tests/tck/issues/4292.test.ts b/packages/graphql/tests/tck/issues/4292.test.ts new file mode 100644 index 00000000000..451365dc788 --- /dev/null +++ b/packages/graphql/tests/tck/issues/4292.test.ts @@ -0,0 +1,276 @@ +/* + * 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/4292", () => { + test("authorization subqueries should be wrapped in a Cypher.CALL", async () => { + const typeDefs = /* GraphQL */ ` + type User { + id: ID! @unique + + email: String! @unique + + name: String + + creator: [Group!]! @relationship(type: "CREATOR_OF", direction: OUT) + + admin: [Admin!]! @relationship(type: "IS_USER", direction: IN) + + contributor: [Contributor!]! @relationship(type: "IS_USER", direction: IN) + + invitations: [Invitee!]! @relationship(type: "CREATOR_OF", direction: OUT) + + roles: [String!]! + } + + type Group { + id: ID! @id @unique + + name: String + + members: [Person!]! @relationship(type: "MEMBER_OF", direction: IN) + + creator: User! + @relationship(type: "CREATOR_OF", direction: IN) + @settable(onCreate: true, onUpdate: true) + + admins: [Admin!]! @relationship(type: "ADMIN_OF", direction: IN) + + contributors: [Contributor!]! @relationship(type: "CONTRIBUTOR_TO", direction: IN) + } + + type Person + @authorization( + validate: [ + { + operations: [CREATE] + where: { node: { group: { creator: { roles_INCLUDES: "plan:paid" } } } } + } + { + operations: [DELETE] + where: { + OR: [ + { node: { creator: { id: "$jwt.uid" } } } + { node: { group: { admins_SOME: { user: { id: "$jwt.uid" } } } } } + { node: { group: { creator: { id: "$jwt.uid" } } } } + ] + } + } + { + operations: [READ, UPDATE] + where: { + OR: [ + { node: { creator: { id: "$jwt.uid" } } } + { node: { group: { admins_SOME: { user: { id: "$jwt.uid" } } } } } + { node: { group: { contributors_SOME: { user: { id: "$jwt.uid" } } } } } + { node: { group: { creator: { id: "$jwt.uid" } } } } + ] + } + } + ] + ) { + id: ID! @id @unique + + name: String! + + creator: User! + @relationship(type: "CREATOR_OF", direction: IN, nestedOperations: [CONNECT]) + @settable(onCreate: true, onUpdate: true) + + group: Group! @relationship(type: "MEMBER_OF", direction: OUT) + + partners: [Person!]! + @relationship( + type: "PARTNER_OF" + queryDirection: UNDIRECTED_ONLY + direction: OUT + properties: "PartnerOf" + ) + } + + enum InviteeRole { + ADMIN + CONTRIBUTOR + } + + enum InviteeStatus { + INVITED + ACCEPTED + } + + interface Invitee { + id: ID! @id + + email: String! + + name: String + + creator: User! @relationship(type: "CREATOR_OF", direction: IN) + + group: Group! @relationship(type: "ADMIN_OF", direction: OUT) + + status: InviteeStatus! @default(value: INVITED) + + user: User @relationship(type: "IS_USER", direction: OUT) + + role: InviteeRole! + } + + type Admin implements Invitee { + id: ID! @unique + group: Group! + creator: User! + email: String! + name: String + status: InviteeStatus! + user: User + role: InviteeRole! @default(value: ADMIN) + } + + type Contributor implements Invitee { + id: ID! @unique + group: Group! @relationship(type: "CONTRIBUTOR_TO", direction: OUT) + creator: User! + email: String! + name: String + status: InviteeStatus! + user: User + role: InviteeRole! @default(value: CONTRIBUTOR) + } + + interface PartnerOf @relationshipProperties { + id: ID! @id + firstDay: Date + lastDay: Date + active: Boolean! @default(value: true) + } + + type JWT @jwt { + roles: [String!]! + } + + type Mutation { + sendInvite(id: ID!, role: InviteeRole!): Boolean! + } + + extend schema @authentication + `; + + const neoSchema = new Neo4jGraphQL({ typeDefs, features: { authorization: { key: "secret" } } }); + + const query = gql` + query Groups { + groups(where: { id: "family_id_1" }) { + id + name + members { + id + name + partnersConnection { + edges { + active + firstDay + lastDay + } + } + } + } + } + `; + + const token = createBearerToken("secret", { roles: ["admin"], id: "something", email: "something" }); + const result = await translateQuery(neoSchema, query, { token }); + + expect(formatCypher(result.cypher)).toMatchInlineSnapshot(` + "MATCH (this:Group) + WHERE this.id = $param0 + CALL { + WITH this + MATCH (this)<-[this0:MEMBER_OF]-(this1:Person) + OPTIONAL MATCH (this1)<-[:CREATOR_OF]-(this2:User) + WITH *, count(this2) AS creatorCount + OPTIONAL MATCH (this1)-[:MEMBER_OF]->(this3:Group) + WITH *, count(this3) AS groupCount + OPTIONAL MATCH (this1)-[:MEMBER_OF]->(this4:Group) + WITH *, count(this4) AS groupCount + WITH * + CALL { + WITH this1 + MATCH (this1)-[:MEMBER_OF]->(this5:Group) + OPTIONAL MATCH (this5)<-[:CREATOR_OF]-(this6:User) + WITH *, count(this6) AS creatorCount + WITH * + WHERE (creatorCount <> 0 AND ($jwt.uid IS NOT NULL AND this6.id = $jwt.uid)) + RETURN count(this5) = 1 AS var7 + } + WITH * + WHERE apoc.util.validatePredicate(NOT ($isAuthenticated = true AND ((creatorCount <> 0 AND ($jwt.uid IS NOT NULL AND this2.id = $jwt.uid)) OR (groupCount <> 0 AND size([(this3)<-[:ADMIN_OF]-(this9:Admin) WHERE single(this8 IN [(this9)-[:IS_USER]->(this8:User) WHERE ($jwt.uid IS NOT NULL AND this8.id = $jwt.uid) | 1] WHERE true) | 1]) > 0) OR (groupCount <> 0 AND size([(this4)<-[:CONTRIBUTOR_TO]-(this11:Contributor) WHERE single(this10 IN [(this11)-[:IS_USER]->(this10:User) WHERE ($jwt.uid IS NOT NULL AND this10.id = $jwt.uid) | 1] WHERE true) | 1]) > 0) OR var7 = true)), \\"@neo4j/graphql/FORBIDDEN\\", [0]) + CALL { + WITH this1 + MATCH (this1)-[this12:PARTNER_OF]-(this13:Person) + OPTIONAL MATCH (this13)<-[:CREATOR_OF]-(this14:User) + WITH *, count(this14) AS creatorCount + OPTIONAL MATCH (this13)-[:MEMBER_OF]->(this15:Group) + WITH *, count(this15) AS groupCount + OPTIONAL MATCH (this13)-[:MEMBER_OF]->(this16:Group) + WITH *, count(this16) AS groupCount + OPTIONAL MATCH (this13)-[:MEMBER_OF]->(this17:Group) + WITH *, count(this17) AS groupCount + WITH * + CALL { + WITH this13 + MATCH (this13)-[:MEMBER_OF]->(this18:Group) + OPTIONAL MATCH (this18)<-[:CREATOR_OF]-(this19:User) + WITH *, count(this19) AS creatorCount + WITH * + WHERE (creatorCount <> 0 AND ($jwt.uid IS NOT NULL AND this19.id = $jwt.uid)) + RETURN count(this18) = 1 AS var20 + } + 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 edges, size(edges) AS totalCount + RETURN { edges: edges, totalCount: totalCount } AS var25 + } + WITH this1 { .id, .name, partnersConnection: var25 } AS this1 + RETURN collect(this1) AS var26 + } + RETURN this { .id, .name, members: var26 } AS this" + `); + + expect(formatParams(result.params)).toMatchInlineSnapshot(` + "{ + \\"param0\\": \\"family_id_1\\", + \\"jwt\\": { + \\"roles\\": [ + \\"admin\\" + ], + \\"id\\": \\"something\\", + \\"email\\": \\"something\\" + }, + \\"isAuthenticated\\": true + }" + `); + }); +}); diff --git a/packages/graphql/tests/tck/simple.test.ts b/packages/graphql/tests/tck/simple.test.ts index a832134c4af..5626f70885b 100644 --- a/packages/graphql/tests/tck/simple.test.ts +++ b/packages/graphql/tests/tck/simple.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("Simple Cypher tests", () => { let typeDefs: DocumentNode; diff --git a/packages/introspector/package.json b/packages/introspector/package.json index fec6ee0923c..947b2c6f40a 100644 --- a/packages/introspector/package.json +++ b/packages/introspector/package.json @@ -37,8 +37,8 @@ "author": "Neo4j Inc.", "devDependencies": { "@neo4j/graphql": "^4.0.0", - "@types/jest": "29.5.9", - "@types/node": "20.9.3", + "@types/jest": "29.5.10", + "@types/node": "20.10.3", "@types/pluralize": "0.0.33", "jest": "29.7.0", "ts-jest": "29.1.1", diff --git a/packages/ogm/CHANGELOG.md b/packages/ogm/CHANGELOG.md index a09ab1eba7f..3327fdfd8a6 100644 --- a/packages/ogm/CHANGELOG.md +++ b/packages/ogm/CHANGELOG.md @@ -1,5 +1,12 @@ # @neo4j/graphql-ogm +## 4.4.4 + +### Patch Changes + +- Updated dependencies [[`226e5ed`](https://github.com/neo4j/graphql/commit/226e5edd22d4bff0767392079bedb58313dd606d), [`24728fe`](https://github.com/neo4j/graphql/commit/24728fedd50a8176c54f67009b2afc84dd91418e), [`c09aa9b`](https://github.com/neo4j/graphql/commit/c09aa9bb1a6ee3d13f918b0fed483893055fb1f1), [`7b310d6`](https://github.com/neo4j/graphql/commit/7b310d6d150c788e04af64f69029740913ddffad), [`1bf0773`](https://github.com/neo4j/graphql/commit/1bf077318d0ddbf730edf53d635f507e36fc7374)]: + - @neo4j/graphql@4.4.4 + ## 4.4.3 ### Patch Changes diff --git a/packages/ogm/package.json b/packages/ogm/package.json index 7b2138e8650..58603cd0a82 100644 --- a/packages/ogm/package.json +++ b/packages/ogm/package.json @@ -1,6 +1,6 @@ { "name": "@neo4j/graphql-ogm", - "version": "4.4.3", + "version": "4.4.4", "description": "GraphQL powered OGM for Neo4j and Javascript applications", "keywords": [ "neo4j", @@ -38,7 +38,7 @@ "@graphql-codegen/plugin-helpers": "^5.0.0", "@graphql-codegen/typescript": "^4.0.0", "@graphql-tools/merge": "^9.0.0", - "@neo4j/graphql": "^4.4.3", + "@neo4j/graphql": "^4.4.4", "prettier": "^2.7.1" }, "peerDependencies": { @@ -46,8 +46,8 @@ "neo4j-driver": "^5.8.0" }, "devDependencies": { - "@types/jest": "29.5.9", - "@types/node": "20.9.3", + "@types/jest": "29.5.10", + "@types/node": "20.10.3", "camelcase": "6.3.0", "graphql-tag": "2.12.6", "jest": "29.7.0", diff --git a/packages/package-tests/babel/package.json b/packages/package-tests/babel/package.json index 3063034af9b..1817a88c2f9 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.3", + "@babel/core": "7.23.5", "@babel/node": "7.22.19", - "@babel/preset-env": "7.23.3" + "@babel/preset-env": "7.23.5" } } diff --git a/renovate.json b/renovate.json index 1ad8291fb58..f9144f7b378 100644 --- a/renovate.json +++ b/renovate.json @@ -1,5 +1,5 @@ { - "extends": ["config:base"], + "extends": ["config:best-practices"], "baseBranches": ["dev"], "rebaseWhen": "auto", "automerge": true, @@ -7,7 +7,11 @@ "automerge": false }, "timezone": "Europe/London", - "schedule": ["after 10pm every weekday", "before 5am every weekday", "every weekend"], + "schedule": [ + "after 10pm every weekday", + "before 5am every weekday", + "every weekend" + ], "ignorePaths": [ "**/node_modules/**", "**/bower_components/**", @@ -34,7 +38,9 @@ }, { "matchPackagePatterns": ["graphql"], - "matchFiles": ["packages/package-tests/graphql-15/package.json"], + "matchFileNames": [ + "packages/package-tests/graphql-15/package.json" + ], "allowedVersions": "15" } ] diff --git a/yarn.lock b/yarn.lock index 66ec503d482..535c5aeec04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -99,15 +99,15 @@ __metadata: languageName: node linkType: hard -"@apollo/composition@npm:2.5.7": - version: 2.5.7 - resolution: "@apollo/composition@npm:2.5.7" +"@apollo/composition@npm:2.6.1": + version: 2.6.1 + resolution: "@apollo/composition@npm:2.6.1" dependencies: - "@apollo/federation-internals": 2.5.7 - "@apollo/query-graphs": 2.5.7 + "@apollo/federation-internals": 2.6.1 + "@apollo/query-graphs": 2.6.1 peerDependencies: graphql: ^16.5.0 - checksum: e5860b93389b57a934711578863ad3150133176c6014a33c30decc9bcfe086952bc1c6339b5e45b2f0f66a9ade5ffc2ef7ba2ef1641caf8abf043a93a37a88c9 + checksum: 3332842e7b320457bedc7d6e99ee76ca16595e8d531cdec33a3deaf53a06932900edea839ec92d9a8b314caff8a5b4021efa21e14c98a291a5da84ee6b5530ca languageName: node linkType: hard @@ -125,9 +125,9 @@ __metadata: languageName: node linkType: hard -"@apollo/federation-internals@npm:2.5.7": - version: 2.5.7 - resolution: "@apollo/federation-internals@npm:2.5.7" +"@apollo/federation-internals@npm:2.6.1": + version: 2.6.1 + resolution: "@apollo/federation-internals@npm:2.6.1" dependencies: "@types/uuid": ^9.0.0 chalk: ^4.1.0 @@ -135,46 +135,47 @@ __metadata: uuid: ^9.0.0 peerDependencies: graphql: ^16.5.0 - checksum: 5ad7da7f4b99aa577f992c3fa61b9e0b686ad94e767fd56299d2ea5f789879c661d1b454b960020a0c06bc2de9e71b27e611e6d8fa5d9e11a775ec844ffa63e7 + checksum: af7293ef180ba2ec5f6300d37ac4240f3cdd8aa966a7cae0e175e52835274f6c89b110b3ab55b9494b380ea02261e0e997be806cea4e4c7ec3a2a0f7b591f900 languageName: node linkType: hard -"@apollo/federation-subgraph-compatibility-tests@npm:2.0.1": - version: 2.0.1 - resolution: "@apollo/federation-subgraph-compatibility-tests@npm:2.0.1" +"@apollo/federation-subgraph-compatibility-tests@npm:2.1.0": + version: 2.1.0 + resolution: "@apollo/federation-subgraph-compatibility-tests@npm:2.1.0" dependencies: - "@apollo/rover": ^0.19.1 + "@apollo/rover": ^0.21.0 debug: ^4.3.4 execa: ^5.1.1 graphql: ^16.8.0 jest: ^29.6.4 make-fetch-happen: ^13.0.0 + mustache: ^4.2.0 pm2: ^5.3.0 ts-jest: ^29.1.1 - checksum: d5783c5e0c029783ea196c11ac6cff5cfbf5d42c167aa7a00c6dd31ac4e87cf28baca744ae82adc370c5505ae1360a4fb975474dbfd141a7bae52b2d86a50e76 + checksum: 10eaa73a2f8726e5a96e2270ed169586e68b1c8c38295602b5f997ed4b4a3d4de32c78bf7d219347b1ef534a3c80e3929026dea0d8723e9ca1f1670d925564c2 languageName: node linkType: hard -"@apollo/federation-subgraph-compatibility@npm:2.0.1": - version: 2.0.1 - resolution: "@apollo/federation-subgraph-compatibility@npm:2.0.1" +"@apollo/federation-subgraph-compatibility@npm:2.1.0": + version: 2.1.0 + resolution: "@apollo/federation-subgraph-compatibility@npm:2.1.0" dependencies: - "@apollo/federation-subgraph-compatibility-tests": 2.0.1 + "@apollo/federation-subgraph-compatibility-tests": 2.1.0 commander: ^11.0.0 debug: ^4.3.4 bin: fedtest: dist/compatibilityTestCommand.js - checksum: 9a6c1d06c6e58745f041bf131efa6704972f0c5935577825321b83565443df38b5830a9d9e265ba2a447bea6a8c57b6f2eeac7163031ea3f1ff7f904a141bf86 + checksum: 7f9bea9f22fd31093bdd1fa930de966128e9436820754b92cb3a25445579b29d91e8bd85968fc85f1a354a77d528746e3a186f771577fd1ee33f736a18ae4f9f languageName: node linkType: hard -"@apollo/gateway@npm:2.5.7": - version: 2.5.7 - resolution: "@apollo/gateway@npm:2.5.7" +"@apollo/gateway@npm:2.6.1": + version: 2.6.1 + resolution: "@apollo/gateway@npm:2.6.1" dependencies: - "@apollo/composition": 2.5.7 - "@apollo/federation-internals": 2.5.7 - "@apollo/query-planner": 2.5.7 + "@apollo/composition": 2.6.1 + "@apollo/federation-internals": 2.6.1 + "@apollo/query-planner": 2.6.1 "@apollo/server-gateway-interface": ^1.1.0 "@apollo/usage-reporting-protobuf": ^4.1.0 "@apollo/utils.createhash": ^2.0.0 @@ -192,7 +193,7 @@ __metadata: node-fetch: ^2.6.7 peerDependencies: graphql: ^16.5.0 - checksum: ecedac02bfbecec4d7f493dd2334334feb0afd99456ffe53017c8afb6ff82ccbcf071e17eff718eb4fe62fca3df41047607135547e115c52ba04f1fa5edf208d + checksum: 01bfc84b7344db334efcc98fc18b9c0ea16389b8051449177c4fab1b746e76b9a7d67ddbf9fc08c48f42732c0579ca4a731272aa68b9bdc0ad839b2d74c1f3d3 languageName: node linkType: hard @@ -219,39 +220,39 @@ __metadata: languageName: node linkType: hard -"@apollo/query-graphs@npm:2.5.7": - version: 2.5.7 - resolution: "@apollo/query-graphs@npm:2.5.7" +"@apollo/query-graphs@npm:2.6.1": + version: 2.6.1 + resolution: "@apollo/query-graphs@npm:2.6.1" dependencies: - "@apollo/federation-internals": 2.5.7 + "@apollo/federation-internals": 2.6.1 deep-equal: ^2.0.5 ts-graphviz: ^1.5.4 uuid: ^9.0.0 peerDependencies: graphql: ^16.5.0 - checksum: 5f5bae32613ba408f88eaf210db649b6c11370fd7556aadb33818ee2290b28fe3ae5ce259aac5bcad7015cabf8ea73c6e90b257e9e07ebc4e381b5e00f0d8eee + checksum: 932acd852c10e2dad8e67902e302602ecaeee5fd1bc98fe4c58bec45fadb20b7a5ba7384f33374e54c08c3e935254b3c88275b6bec9186c378fb89aa578a4853 languageName: node linkType: hard -"@apollo/query-planner@npm:2.5.7": - version: 2.5.7 - resolution: "@apollo/query-planner@npm:2.5.7" +"@apollo/query-planner@npm:2.6.1": + version: 2.6.1 + resolution: "@apollo/query-planner@npm:2.6.1" dependencies: - "@apollo/federation-internals": 2.5.7 - "@apollo/query-graphs": 2.5.7 + "@apollo/federation-internals": 2.6.1 + "@apollo/query-graphs": 2.6.1 "@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: 3c8950f2540f5e1fae58ac25d889683e201867eaf3124dd336c78bb727c06eb650cae5d9fd9a504be97d346c61408998424a156c37c22e00b45d13146ff3014b + checksum: 14c8cf1447241d6b2273be8de12c54e45cb963d49807ac9af4ac906e2edb70e8c962ae283c766700333cef56d14dcfaca8741d60ea2070d994fb1a8f50023f43 languageName: node linkType: hard -"@apollo/rover@npm:^0.19.1": - version: 0.19.1 - resolution: "@apollo/rover@npm:0.19.1" +"@apollo/rover@npm:^0.21.0": + version: 0.21.0 + resolution: "@apollo/rover@npm:0.21.0" dependencies: axios-proxy-builder: ^0.1.1 binary-install: ^1.0.6 @@ -259,7 +260,7 @@ __metadata: detect-libc: ^2.0.0 bin: rover: run.js - checksum: 35da5ceaeec832030bf5b425aa6127771b594a64fa70e26d50139fe8000291f76d1781df23ee5cf46b8579c5480f2b5992c90db6122d697331c371c88b30a304 + checksum: e96fae284559d012ddeb823713649a9f0975051cac5a5305c998b288d0eac2b871129d10d455f0f5b7f135387df54e844f33c0dafd6499de90e80e9f7d99ea01 languageName: node linkType: hard @@ -1296,15 +1297,15 @@ __metadata: languageName: node linkType: hard -"@changesets/apply-release-plan@npm:^6.1.4": - version: 6.1.4 - resolution: "@changesets/apply-release-plan@npm:6.1.4" +"@changesets/apply-release-plan@npm:^7.0.0": + version: 7.0.0 + resolution: "@changesets/apply-release-plan@npm:7.0.0" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/config": ^2.3.1 - "@changesets/get-version-range-type": ^0.3.2 - "@changesets/git": ^2.0.0 - "@changesets/types": ^5.2.1 + "@changesets/config": ^3.0.0 + "@changesets/get-version-range-type": ^0.4.0 + "@changesets/git": ^3.0.0 + "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 detect-indent: ^6.0.0 fs-extra: ^7.0.1 @@ -1313,72 +1314,71 @@ __metadata: prettier: ^2.7.1 resolve-from: ^5.0.0 semver: ^7.5.3 - checksum: d386aee70c5483c97d964c6fa1191878005b7050d34b2e1e4a1ad66d9ad44f8f20d1c884e01e770b954bd2d4364f935510e53ae896212669f67e5c37b2a610c7 + checksum: ad83f89a3d46cd5249fa960cb0324114532bd5f25e74466d181afd6661273824859d038a12ba587a5e044f9169810e4a6febbb61e23c3819b3b28c00176a8bdf languageName: node linkType: hard -"@changesets/assemble-release-plan@npm:^5.2.4": - version: 5.2.4 - resolution: "@changesets/assemble-release-plan@npm:5.2.4" +"@changesets/assemble-release-plan@npm:^6.0.0": + version: 6.0.0 + resolution: "@changesets/assemble-release-plan@npm:6.0.0" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/errors": ^0.1.4 - "@changesets/get-dependents-graph": ^1.3.6 - "@changesets/types": ^5.2.1 + "@changesets/errors": ^0.2.0 + "@changesets/get-dependents-graph": ^2.0.0 + "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 semver: ^7.5.3 - checksum: 32f443a0afec3d5a4afc68c8de32e8ff88531ea24976b50583b1d6870d71cec2729f27952af82854eb54e2ad0a619872d211d654c596ee0eb42c83ab54ad15ae + checksum: 0e6d25f25e0e3cc0e92aa8c43f5f496bae9464e2523be4ff81e31b6c9971b63bb1264821a2483c48d451d89d60af1acebe727e7f8c392ed48188a3ff26d0950e languageName: node linkType: hard -"@changesets/changelog-git@npm:^0.1.14": - version: 0.1.14 - resolution: "@changesets/changelog-git@npm:0.1.14" +"@changesets/changelog-git@npm:^0.2.0": + version: 0.2.0 + resolution: "@changesets/changelog-git@npm:0.2.0" dependencies: - "@changesets/types": ^5.2.1 - checksum: 60b45bb899e66cec669ab3884d5d18550cd30bf5a8b06f335eb72aa6c9e018dd3e0187e4df61c91a22076153e346b735b792f0e9c6186e6245b1b7aec2fc42d4 + "@changesets/types": ^6.0.0 + checksum: 132660f7fdabbdda00ac803cc822d6427a1a38a17a5f414e87ad32f6dc4cbef5280a147ecdc087a28dc06c8bd0762f8d6e7132d01b8a4142b59fbe1bc2177034 languageName: node linkType: hard -"@changesets/changelog-github@npm:0.4.8": - version: 0.4.8 - resolution: "@changesets/changelog-github@npm:0.4.8" +"@changesets/changelog-github@npm:0.5.0": + version: 0.5.0 + resolution: "@changesets/changelog-github@npm:0.5.0" dependencies: - "@changesets/get-github-info": ^0.5.2 - "@changesets/types": ^5.2.1 + "@changesets/get-github-info": ^0.6.0 + "@changesets/types": ^6.0.0 dotenv: ^8.1.0 - checksum: 8a357cc08757e0eeca267ee05141f68bef936582abef8b78a5d30d99f5a86e41b7d3debba70992b73b2f57b0fc6201ec1cc3c65116930167ee3197b427b865c5 + checksum: 4ab43d8104693f970d878f2b1657ff67b4d4dcb7452ddf118575153bab74286cdfd125381c2ab92b205bce4b2c653c36552138bf2900f7165ac39a868b7fe22c languageName: node linkType: hard -"@changesets/cli@npm:2.26.2": - version: 2.26.2 - resolution: "@changesets/cli@npm:2.26.2" +"@changesets/cli@npm:2.27.1": + version: 2.27.1 + resolution: "@changesets/cli@npm:2.27.1" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/apply-release-plan": ^6.1.4 - "@changesets/assemble-release-plan": ^5.2.4 - "@changesets/changelog-git": ^0.1.14 - "@changesets/config": ^2.3.1 - "@changesets/errors": ^0.1.4 - "@changesets/get-dependents-graph": ^1.3.6 - "@changesets/get-release-plan": ^3.0.17 - "@changesets/git": ^2.0.0 - "@changesets/logger": ^0.0.5 - "@changesets/pre": ^1.0.14 - "@changesets/read": ^0.5.9 - "@changesets/types": ^5.2.1 - "@changesets/write": ^0.2.3 + "@changesets/apply-release-plan": ^7.0.0 + "@changesets/assemble-release-plan": ^6.0.0 + "@changesets/changelog-git": ^0.2.0 + "@changesets/config": ^3.0.0 + "@changesets/errors": ^0.2.0 + "@changesets/get-dependents-graph": ^2.0.0 + "@changesets/get-release-plan": ^4.0.0 + "@changesets/git": ^3.0.0 + "@changesets/logger": ^0.1.0 + "@changesets/pre": ^2.0.0 + "@changesets/read": ^0.6.0 + "@changesets/types": ^6.0.0 + "@changesets/write": ^0.3.0 "@manypkg/get-packages": ^1.1.3 - "@types/is-ci": ^3.0.0 "@types/semver": ^7.5.0 ansi-colors: ^4.1.3 chalk: ^2.1.0 + ci-info: ^3.7.0 enquirer: ^2.3.0 external-editor: ^3.1.0 fs-extra: ^7.0.1 human-id: ^1.0.2 - is-ci: ^3.0.1 meow: ^6.0.0 outdent: ^0.5.0 p-limit: ^2.2.0 @@ -1390,139 +1390,139 @@ __metadata: tty-table: ^4.1.5 bin: changeset: bin.js - checksum: fc7b5bf319b19abed7a8d33a9fbd9ce49108af61c9c51920f609a49cb0c557f0b998711250d0cac149d0bed8a522f3109c4d8b0dda65b96ff2f823d16ca2f972 + checksum: 0d030dec7e0ef28626082a257d57f46cdf65edb65a95f5a3511a9d298ca052388d8ab7f9a714943864eddc59148c4afb0b802a9c75b5bea45aade4c0dc7a5fa6 languageName: node linkType: hard -"@changesets/config@npm:^2.3.1": - version: 2.3.1 - resolution: "@changesets/config@npm:2.3.1" +"@changesets/config@npm:^3.0.0": + version: 3.0.0 + resolution: "@changesets/config@npm:3.0.0" dependencies: - "@changesets/errors": ^0.1.4 - "@changesets/get-dependents-graph": ^1.3.6 - "@changesets/logger": ^0.0.5 - "@changesets/types": ^5.2.1 + "@changesets/errors": ^0.2.0 + "@changesets/get-dependents-graph": ^2.0.0 + "@changesets/logger": ^0.1.0 + "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 fs-extra: ^7.0.1 micromatch: ^4.0.2 - checksum: 8af58e3add4751ac8ce2c01f026ac8843b8d1c07c9a3df6518496eaef67f56458a84cad310763c588f7eccbf6831afbf280df7e05e78b294027b6b847be3d0cc + checksum: 31a8c37e38768cf3676d24b7d371009dd1d691f221ecf086b79f0d96dc8e95aa408cda3659eb867a14615ea38a1c2be448bf0655c7570539af57c930ca784051 languageName: node linkType: hard -"@changesets/errors@npm:^0.1.4": - version: 0.1.4 - resolution: "@changesets/errors@npm:0.1.4" +"@changesets/errors@npm:^0.2.0": + version: 0.2.0 + resolution: "@changesets/errors@npm:0.2.0" dependencies: extendable-error: ^0.1.5 - checksum: 10734f1379715bf5a70b566dd42b50a75964d76f382bb67332776614454deda6d04a43dd7e727cd7cba56d7f2f7c95a07c7c0a19dd5d64fb1980b28322840733 + checksum: 4b79373f92287af4f723e8dbbccaf0299aa8735fc043243d0ad587f04a7614615ea50180be575d4438b9f00aa82d1cf85e902b77a55bdd3e0a8dd97e77b18c60 languageName: node linkType: hard -"@changesets/get-dependents-graph@npm:^1.3.6": - version: 1.3.6 - resolution: "@changesets/get-dependents-graph@npm:1.3.6" +"@changesets/get-dependents-graph@npm:^2.0.0": + version: 2.0.0 + resolution: "@changesets/get-dependents-graph@npm:2.0.0" dependencies: - "@changesets/types": ^5.2.1 + "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 chalk: ^2.1.0 fs-extra: ^7.0.1 semver: ^7.5.3 - checksum: d2cbbc5041063b939899502d1b264a0d9edb655acefd7f6197883229156bb7cfd1ace642ae4a1f7f7b432f2c51429f5dc9851ff5a9ed47f1c0159916e66627a9 + checksum: 6690d3ed36e8a636bc2a985d209bd72ee1100601ccf00850ca1fbe8500af839a3f4e5bd2167858cf11383aa76360f853e481533157060ad882fb56319db3090a languageName: node linkType: hard -"@changesets/get-github-info@npm:^0.5.2": - version: 0.5.2 - resolution: "@changesets/get-github-info@npm:0.5.2" +"@changesets/get-github-info@npm:^0.6.0": + version: 0.6.0 + resolution: "@changesets/get-github-info@npm:0.6.0" dependencies: dataloader: ^1.4.0 node-fetch: ^2.5.0 - checksum: 067e07eeaecdbedbd1c715513c4aa6206a941bd1d3af292d067792808c6fa6644caad2b35fba614a44892559c031c234df8028f8d2abd4cb2682d48080ef5df3 + checksum: 753173bda536aa79cb0502f59ce13889b23ae8463d04893d43ff22966818060837d9db4052b6cbfbd95dfb242fbfd38890a38c56832948e83bf358a47812b708 languageName: node linkType: hard -"@changesets/get-release-plan@npm:^3.0.17": - version: 3.0.17 - resolution: "@changesets/get-release-plan@npm:3.0.17" +"@changesets/get-release-plan@npm:^4.0.0": + version: 4.0.0 + resolution: "@changesets/get-release-plan@npm:4.0.0" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/assemble-release-plan": ^5.2.4 - "@changesets/config": ^2.3.1 - "@changesets/pre": ^1.0.14 - "@changesets/read": ^0.5.9 - "@changesets/types": ^5.2.1 + "@changesets/assemble-release-plan": ^6.0.0 + "@changesets/config": ^3.0.0 + "@changesets/pre": ^2.0.0 + "@changesets/read": ^0.6.0 + "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 - checksum: 8a0e3794d0f1e6220d173dbec96352ad69b585d013c3183888ca598dfdfcaa8a5ac3f7f36d5c511575cdc3559c2ad6f8cecfaa16ba9c24380899a81daa7af924 + checksum: 57672c1e94f95de8ac65aac969275e0cb225f02aa86b2cef69329fff6e36ba5fde04eadeb6af36f4d8ac41a8fd329028b4df4c23c15c10fd13e026c77463d576 languageName: node linkType: hard -"@changesets/get-version-range-type@npm:^0.3.2": - version: 0.3.2 - resolution: "@changesets/get-version-range-type@npm:0.3.2" - checksum: b7ee7127c472a3886906ca6db336ac11233a5e75abc882084bfb4794e79a8936e3faceec3c04bf61c26453cd7f74278d9bf22aea4cdca8c1cd992591925b3c9b +"@changesets/get-version-range-type@npm:^0.4.0": + version: 0.4.0 + resolution: "@changesets/get-version-range-type@npm:0.4.0" + checksum: 2e8c511e658e193f48de7f09522649c4cf072932f0cbe0f252a7f2703d7775b0b90b632254526338795d0658e340be9dff3879cfc8eba4534b8cd6071efff8c9 languageName: node linkType: hard -"@changesets/git@npm:^2.0.0": - version: 2.0.0 - resolution: "@changesets/git@npm:2.0.0" +"@changesets/git@npm:^3.0.0": + version: 3.0.0 + resolution: "@changesets/git@npm:3.0.0" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/errors": ^0.1.4 - "@changesets/types": ^5.2.1 + "@changesets/errors": ^0.2.0 + "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 is-subdir: ^1.1.1 micromatch: ^4.0.2 spawndamnit: ^2.0.0 - checksum: 3820b7b689bbe8dfb93222c766bee214e68a45f07b2b5c8056891f9ffe6f1e369c0f84388246a9eea5317b496ae80ffd1508319190f79c359f060ebf8ccb7b13 + checksum: a8fa66d77302b50d5e604aca898ee813247537d23a05004637ecee4aa1579d6a2859283c099bdcf3e2b232258c93ff81dd57aa867858788e457df40118c64c2b languageName: node linkType: hard -"@changesets/logger@npm:^0.0.5": - version: 0.0.5 - resolution: "@changesets/logger@npm:0.0.5" +"@changesets/logger@npm:^0.1.0": + version: 0.1.0 + resolution: "@changesets/logger@npm:0.1.0" dependencies: chalk: ^2.1.0 - checksum: bfec3cd9122b00c0ec25e96730f771ffd662ef3906d571bad1e4e9993f9d54d357d3eaf074b3dfaa4e23af759ce68efa2a97d8b845b0d8c951df5d21c6dfdff5 + checksum: d8ef1b7caf3d2c15a9e7743b7a9462e0c2e61c76d9a5bbed5eff805afa8226117505309c6e9095001136b4f6d9ae0aba61377e53af8aa0809f1febd1b5f787f1 languageName: node linkType: hard -"@changesets/parse@npm:^0.3.16": - version: 0.3.16 - resolution: "@changesets/parse@npm:0.3.16" +"@changesets/parse@npm:^0.4.0": + version: 0.4.0 + resolution: "@changesets/parse@npm:0.4.0" dependencies: - "@changesets/types": ^5.2.1 + "@changesets/types": ^6.0.0 js-yaml: ^3.13.1 - checksum: 475f808ac8d33ec90af3914d55af1da8eeb9336d6cab7dd9e5be74af844f0ec04f4a67d5237a1d3284a468e0c9198e2be01d0e5870a1b28e63bc240f5f1ffea9 + checksum: 3dd970b244479746233ebd357cfff3816cf9f344ebf2cf0c7c55ce8579adfd3f506978e86ad61222dc3acf1548a2105ffdd8b3e940b3f82b225741315cee2bf0 languageName: node linkType: hard -"@changesets/pre@npm:^1.0.14": - version: 1.0.14 - resolution: "@changesets/pre@npm:1.0.14" +"@changesets/pre@npm:^2.0.0": + version: 2.0.0 + resolution: "@changesets/pre@npm:2.0.0" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/errors": ^0.1.4 - "@changesets/types": ^5.2.1 + "@changesets/errors": ^0.2.0 + "@changesets/types": ^6.0.0 "@manypkg/get-packages": ^1.1.3 fs-extra: ^7.0.1 - checksum: 6b849bd6f916476a5b5664bc4286020bee506985c82f723a757fa4e681b0b7129db81751f16072ac55a980ffd83a4b234d6b8d0f8b6bc889aa0c0fd5377431e8 + checksum: 6a01086405f4e4ce63abb8f222de39b69a5762c9c8c8f19c0d3c72f7798248d7a152937028f1be24be1f8a4a5e47e4cb23c54bc36f979539b24a728c893caf4e languageName: node linkType: hard -"@changesets/read@npm:^0.5.9": - version: 0.5.9 - resolution: "@changesets/read@npm:0.5.9" +"@changesets/read@npm:^0.6.0": + version: 0.6.0 + resolution: "@changesets/read@npm:0.6.0" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/git": ^2.0.0 - "@changesets/logger": ^0.0.5 - "@changesets/parse": ^0.3.16 - "@changesets/types": ^5.2.1 + "@changesets/git": ^3.0.0 + "@changesets/logger": ^0.1.0 + "@changesets/parse": ^0.4.0 + "@changesets/types": ^6.0.0 chalk: ^2.1.0 fs-extra: ^7.0.1 p-filter: ^2.1.0 - checksum: 0875a80829186de2da55bc0347601cc31b269d54fb6967a5093abacbbd9f949e352907b8340b61348a304228fdade670ded151327f16eea3424b5b4b2bb9888c + checksum: 3da6428124b4983f6ccbdae324c73044cd6a84269bfdbaff545331042e3d6845c647613b5d8f4ffdd48bad5b791623eca2be1b507652ea47b77e136cd2e26c70 languageName: node linkType: hard @@ -1533,23 +1533,23 @@ __metadata: languageName: node linkType: hard -"@changesets/types@npm:^5.2.1": - version: 5.2.1 - resolution: "@changesets/types@npm:5.2.1" - checksum: 527dc1aa41b040fe35bcd55f7d07bec710320b179b000c429723e25b87aac18be487daf5047d4fecf2781aad78f73abff111e76e411b652f7a2e812a464c69f2 +"@changesets/types@npm:^6.0.0": + version: 6.0.0 + resolution: "@changesets/types@npm:6.0.0" + checksum: d528b5d712f62c26ea422c7d34ccf6eac57a353c0733d96716db3c796ecd9bba5d496d48b37d5d46b784dc45b69c06ce3345fa3515df981bb68456cad68e6465 languageName: node linkType: hard -"@changesets/write@npm:^0.2.3": - version: 0.2.3 - resolution: "@changesets/write@npm:0.2.3" +"@changesets/write@npm:^0.3.0": + version: 0.3.0 + resolution: "@changesets/write@npm:0.3.0" dependencies: "@babel/runtime": ^7.20.1 - "@changesets/types": ^5.2.1 + "@changesets/types": ^6.0.0 fs-extra: ^7.0.1 human-id: ^1.0.2 prettier: ^2.7.1 - checksum: 40ad8069f9adc565b78a5f25992e31b41a12e551d94c29e1b4def49ce98871a1e358feda6536be8b363a6dba18b1226a22ecfc60fdd7bc1e74bfcf46b07f91be + checksum: 37588eb3ef2af15b3ea09d46864c994780619d20b791ea5b654801a035a3a12540c7f953e6e4f36731678615edc6d1c32f8fe174d599d3e6ce2d68263865788b languageName: node linkType: hard @@ -1702,9 +1702,9 @@ __metadata: languageName: node linkType: hard -"@codemirror/autocomplete@npm:6.11.0": - version: 6.11.0 - resolution: "@codemirror/autocomplete@npm:6.11.0" +"@codemirror/autocomplete@npm:6.11.1": + version: 6.11.1 + resolution: "@codemirror/autocomplete@npm:6.11.1" dependencies: "@codemirror/language": ^6.0.0 "@codemirror/state": ^6.0.0 @@ -1715,7 +1715,7 @@ __metadata: "@codemirror/state": ^6.0.0 "@codemirror/view": ^6.0.0 "@lezer/common": ^1.0.0 - checksum: f80ac2c49b3736bdcce8d16776c09bfa3fe85a3ce486bfb96aa07157aff9e7afb3f96575a1d83cee96a97439c0ef0039325901afb811a9f37734ea0a1b965c82 + checksum: 69cb77d51dbc4c76a990fb8e562075d6fa11b2aef00fce33d2a98dd701f6a89050b1b464ae8ee1e2cbe1a4210522b1a3c2260cdf5c933a062093acaf98a5eedc languageName: node linkType: hard @@ -1736,15 +1736,15 @@ __metadata: languageName: node linkType: hard -"@codemirror/commands@npm:6.3.0": - version: 6.3.0 - resolution: "@codemirror/commands@npm:6.3.0" +"@codemirror/commands@npm:6.3.2": + version: 6.3.2 + resolution: "@codemirror/commands@npm:6.3.2" dependencies: "@codemirror/language": ^6.0.0 "@codemirror/state": ^6.2.0 "@codemirror/view": ^6.0.0 "@lezer/common": ^1.1.0 - checksum: d6ade0ba7d4f80c2e44163935783d2f2f35c8b641a4b4f62452c0630211670abe5093786cf5a4af14147102d4284dae660a26f3ae58fd840e838685a81107d11 + checksum: 683c444d8e6ad889ab5efd0d742b0fa28b78c8cad63276ec60d298b13d4939c8bd7e1d6fd3535645b8d255147de0d3aef46d89a29c19d0af58a7f2914bdcb3ab languageName: node linkType: hard @@ -1775,9 +1775,9 @@ __metadata: languageName: node linkType: hard -"@codemirror/language@npm:6.9.2": - version: 6.9.2 - resolution: "@codemirror/language@npm:6.9.2" +"@codemirror/language@npm:6.9.3": + version: 6.9.3 + resolution: "@codemirror/language@npm:6.9.3" dependencies: "@codemirror/state": ^6.0.0 "@codemirror/view": ^6.0.0 @@ -1785,7 +1785,7 @@ __metadata: "@lezer/highlight": ^1.0.0 "@lezer/lr": ^1.0.0 style-mod: ^4.0.0 - checksum: eee7b861b5591114cac7502cd532d5b923639740081a4cd7e28696c252af8d759b14686aaf6d5eee7e0969ff647b7aaf03a5eea7235fb6d9858ee19433f1c74d + checksum: 774a40bc91c748d418a9a774161a5b083061124e4439bb753072bc657ec4c4784f595161c10c7c3935154b22291bf6dc74c9abe827033db32e217ac3963478f3 languageName: node linkType: hard @@ -2112,9 +2112,9 @@ __metadata: languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.1.3": - version: 2.1.3 - resolution: "@eslint/eslintrc@npm:2.1.3" +"@eslint/eslintrc@npm:^2.1.4": + version: 2.1.4 + resolution: "@eslint/eslintrc@npm:2.1.4" dependencies: ajv: ^6.12.4 debug: ^4.3.2 @@ -2125,14 +2125,14 @@ __metadata: js-yaml: ^4.1.0 minimatch: ^3.1.2 strip-json-comments: ^3.1.1 - checksum: 5c6c3878192fe0ddffa9aff08b4e2f3bcc8f1c10d6449b7295a5f58b662019896deabfc19890455ffd7e60a5bd28d25d0eaefb2f78b2d230aae3879af92b89e5 + checksum: 10957c7592b20ca0089262d8c2a8accbad14b4f6507e35416c32ee6b4dbf9cad67dfb77096bbd405405e9ada2b107f3797fe94362e1c55e0b09d6e90dd149127 languageName: node linkType: hard -"@eslint/js@npm:8.54.0": - version: 8.54.0 - resolution: "@eslint/js@npm:8.54.0" - checksum: 6d88a6f711ef0133566b5340e3178a178fbb297585766460f195d0a9db85688f1e5cf8559fd5748aeb3131e2096c66595b323d8edab22df015acda68f1ebde92 +"@eslint/js@npm:8.55.0": + version: 8.55.0 + resolution: "@eslint/js@npm:8.55.0" + checksum: fa33ef619f0646ed15649b0c2e313e4d9ccee8425884bdbfc78020d6b6b64c0c42fa9d83061d0e6158e1d4274f03f0f9008786540e2efab8fcdc48082259908c languageName: node linkType: hard @@ -2576,6 +2576,18 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/merge@npm:^9.0.1": + version: 9.0.1 + resolution: "@graphql-tools/merge@npm:9.0.1" + dependencies: + "@graphql-tools/utils": ^10.0.10 + tslib: ^2.4.0 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: f078628838f57dcd2988b46ec27ce4786daef6e7fdd07c012acec2fe52139f4a905a101883eb0fa7094d1ace6d1b10e6a8d40c03778496b50e85093b36316e4e + languageName: node + linkType: hard + "@graphql-tools/optimize@npm:^2.0.0": version: 2.0.0 resolution: "@graphql-tools/optimize@npm:2.0.0" @@ -2614,17 +2626,17 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/schema@npm:10.0.0, @graphql-tools/schema@npm:^10.0.0": - version: 10.0.0 - resolution: "@graphql-tools/schema@npm:10.0.0" +"@graphql-tools/schema@npm:10.0.2": + version: 10.0.2 + resolution: "@graphql-tools/schema@npm:10.0.2" dependencies: - "@graphql-tools/merge": ^9.0.0 - "@graphql-tools/utils": ^10.0.0 + "@graphql-tools/merge": ^9.0.1 + "@graphql-tools/utils": ^10.0.10 tslib: ^2.4.0 value-or-promise: ^1.0.12 peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 550e9a4528584a4d108892f1553fb5b2590e63e88b9a9d3c1ad80b01c974ca9947adb9d1448a6969230d90c15dc96e8e84d62f32ef0fde804c389b43ac5bd739 + checksum: fe977b1aee05b0a88cf6bb029f17d828d8707f784e1d42d446984b6ba649d78e16e3295c549ee352c09bbe88ad87c23bbe04b946c096b6815156c5be80d79a3f languageName: node linkType: hard @@ -2642,6 +2654,20 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/schema@npm:^10.0.0": + version: 10.0.0 + resolution: "@graphql-tools/schema@npm:10.0.0" + dependencies: + "@graphql-tools/merge": ^9.0.0 + "@graphql-tools/utils": ^10.0.0 + tslib: ^2.4.0 + value-or-promise: ^1.0.12 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 550e9a4528584a4d108892f1553fb5b2590e63e88b9a9d3c1ad80b01c974ca9947adb9d1448a6969230d90c15dc96e8e84d62f32ef0fde804c389b43ac5bd739 + languageName: node + linkType: hard + "@graphql-tools/schema@npm:^9.0.0, @graphql-tools/schema@npm:^9.0.18": version: 9.0.19 resolution: "@graphql-tools/schema@npm:9.0.19" @@ -2680,6 +2706,20 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/utils@npm:^10.0.10": + version: 10.0.10 + resolution: "@graphql-tools/utils@npm:10.0.10" + dependencies: + "@graphql-typed-document-node/core": ^3.1.1 + cross-inspect: 1.0.0 + dset: ^3.1.2 + tslib: ^2.4.0 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 2b23dcf131901eec196b6b2d2a6c57c66b4b2c21eb1a1e22b9bd6e6cb53b9cf9ca630cca633398ae4a6e4aed781ed6fdaf2d93e990836c15ffec44ac1de50aa1 + languageName: node + linkType: hard + "@graphql-tools/utils@npm:^9.2.1": version: 9.2.1 resolution: "@graphql-tools/utils@npm:9.2.1" @@ -3485,9 +3525,9 @@ __metadata: languageName: node linkType: hard -"@neo4j-ndl/react@npm:2.0.12": - version: 2.0.12 - resolution: "@neo4j-ndl/react@npm:2.0.12" +"@neo4j-ndl/react@npm:2.0.14": + version: 2.0.14 + resolution: "@neo4j-ndl/react@npm:2.0.14" dependencies: "@floating-ui/react": 0.25.1 "@heroicons/react": 2.0.13 @@ -3512,7 +3552,7 @@ __metadata: peerDependencies: "@heroicons/react": 2.0.13 react: ">=16.8.0" - checksum: f6b987e49253bb5997d8859f2094a02a6c64f67110f3948b3adf3b9c6ae2118cf8cad9fff0f39e9be0ea87cdcddde1c2763b4362d1c4cb2444aea1697e8701ea + checksum: be5ab15da9f8fe4b57310caf01972e15de83dc42f60abb9f731410385429db116cd388d1bb934cd6bac64995c168a0f12251ca81f38b080f34e34e18c80fea63 languageName: node linkType: hard @@ -3533,15 +3573,15 @@ __metadata: "@types/body-parser": 1.19.5 "@types/cors": 2.8.17 "@types/debug": 4.1.12 - "@types/jest": 29.5.9 - "@types/node": 20.9.3 + "@types/jest": 29.5.10 + "@types/node": 20.10.3 amqplib: 0.10.3 body-parser: ^1.20.2 camelcase: 6.3.0 cors: ^2.8.5 graphql-ws: 5.14.2 jest: 29.7.0 - neo4j-driver: 5.14.0 + neo4j-driver: 5.15.0 pluralize: 8.0.0 randomstring: 1.3.0 supertest: 6.3.3 @@ -3561,9 +3601,9 @@ __metadata: "@graphql-codegen/plugin-helpers": ^5.0.0 "@graphql-codegen/typescript": ^4.0.0 "@graphql-tools/merge": ^9.0.0 - "@neo4j/graphql": ^4.4.3 - "@types/jest": 29.5.9 - "@types/node": 20.9.3 + "@neo4j/graphql": ^4.4.4 + "@types/jest": 29.5.10 + "@types/node": 20.10.3 camelcase: 6.3.0 graphql-tag: 2.12.6 jest: 29.7.0 @@ -3586,25 +3626,25 @@ __metadata: version: 0.0.0-use.local resolution: "@neo4j/graphql-toolbox@workspace:packages/graphql-toolbox" dependencies: - "@codemirror/autocomplete": 6.11.0 - "@codemirror/commands": 6.3.0 + "@codemirror/autocomplete": 6.11.1 + "@codemirror/commands": 6.3.2 "@codemirror/lang-javascript": 6.2.1 - "@codemirror/language": 6.9.2 + "@codemirror/language": 6.9.3 "@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.12 - "@neo4j/graphql": 4.4.3 + "@neo4j-ndl/react": 2.0.14 + "@neo4j/graphql": 4.4.4 "@neo4j/introspector": 2.0.0 - "@playwright/test": 1.40.0 + "@playwright/test": 1.40.1 "@tsconfig/create-react-app": 2.0.1 - "@types/codemirror": 5.60.14 + "@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.15 + "@types/react-dom": 18.2.17 "@types/webpack": 5.28.5 autoprefixer: 10.4.16 classnames: 2.3.2 @@ -3626,10 +3666,10 @@ __metadata: jest: 29.7.0 jest-environment-jsdom: 29.7.0 markdown-it: 13.0.2 - neo4j-driver: 5.14.0 + neo4j-driver: 5.15.0 node-polyfill-webpack-plugin: 2.0.1 parse5: 7.1.2 - postcss: 8.4.31 + postcss: 8.4.32 postcss-loader: 7.3.3 prettier: 3.0.0 process: 0.11.10 @@ -3649,33 +3689,33 @@ __metadata: webpack-cli: 5.1.4 webpack-dev-server: 4.15.1 webpack-notifier: 1.15.0 - zustand: 4.4.6 + zustand: 4.4.7 languageName: unknown linkType: soft -"@neo4j/graphql@4.4.3, @neo4j/graphql@^4.0.0, @neo4j/graphql@^4.0.0-beta.0, @neo4j/graphql@^4.4.3, @neo4j/graphql@workspace:packages/graphql": +"@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": version: 0.0.0-use.local resolution: "@neo4j/graphql@workspace:packages/graphql" dependencies: - "@apollo/gateway": 2.5.7 + "@apollo/gateway": 2.6.1 "@apollo/server": 4.9.5 "@apollo/subgraph": ^2.2.3 "@faker-js/faker": 8.3.1 "@graphql-tools/merge": ^9.0.0 "@graphql-tools/resolvers-composition": ^7.0.0 - "@graphql-tools/schema": 10.0.0 + "@graphql-tools/schema": 10.0.2 "@graphql-tools/utils": ^10.0.0 "@neo4j/cypher-builder": ^1.7.1 "@types/deep-equal": 1.0.4 "@types/is-uuid": 1.0.2 - "@types/jest": 29.5.9 + "@types/jest": 29.5.10 "@types/jsonwebtoken": 9.0.5 - "@types/node": 20.9.3 + "@types/node": 20.10.3 "@types/pluralize": 0.0.33 "@types/randomstring": 1.1.11 - "@types/semver": 7.5.5 + "@types/semver": 7.5.6 "@types/supertest": 2.0.16 - "@types/ws": 8.5.9 + "@types/ws": 8.5.10 camelcase: ^6.3.0 debug: ^4.3.4 dedent: 1.5.1 @@ -3697,7 +3737,7 @@ __metadata: koa-router: 12.0.1 libnpmsearch: 7.0.0 mock-jwks: 1.0.10 - nock: 13.3.8 + nock: 13.4.0 npm-run-all: 4.1.5 pluralize: ^8.0.0 randomstring: 1.3.0 @@ -3727,8 +3767,8 @@ __metadata: resolution: "@neo4j/introspector@workspace:packages/introspector" dependencies: "@neo4j/graphql": ^4.0.0 - "@types/jest": 29.5.9 - "@types/node": 20.9.3 + "@types/jest": 29.5.10 + "@types/node": 20.10.3 "@types/pluralize": 0.0.33 camelcase: ^6.3.0 debug: ^4.3.4 @@ -4714,14 +4754,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:1.40.0": - version: 1.40.0 - resolution: "@playwright/test@npm:1.40.0" +"@playwright/test@npm:1.40.1": + version: 1.40.1 + resolution: "@playwright/test@npm:1.40.1" dependencies: - playwright: 1.40.0 + playwright: 1.40.1 bin: playwright: cli.js - checksum: 128f05978f9f5a557f0b7924ec134d43cb70c78d74bc3bf7b18576f00e72399100ddf1f4a139e05ea8275407d8e27be0203ac34f514319a2cbeb01eaf0be5be4 + checksum: ae094e6cb809365c0707ee2b184e42d2a2542569ada020d2d44ca5866066941262bd9a67af185f86c2fb0133c9b712ea8cb73e2959a289e4261c5fd17077283c languageName: node linkType: hard @@ -7256,12 +7296,12 @@ __metadata: languageName: node linkType: hard -"@types/codemirror@npm:5.60.14": - version: 5.60.14 - resolution: "@types/codemirror@npm:5.60.14" +"@types/codemirror@npm:5.60.15": + version: 5.60.15 + resolution: "@types/codemirror@npm:5.60.15" dependencies: "@types/tern": "*" - checksum: b9d8e5fb62e5441b4842c1fcd85dab2d671aff56f6f636dd1c09ac43316f9a9ded3f83782b1f3ecd16e34f1169b1c2743e6b28ea95733925115839512332e3f6 + checksum: cfad3f569de48fba3efa44fdfeba77933e231486a52cc80cff7ce6eeeed5b447a5bc2b11e2226bc00ccee332c661e53e35a15cf14eb835f434a6a402d9462f5f languageName: node linkType: hard @@ -7433,15 +7473,6 @@ __metadata: languageName: node linkType: hard -"@types/is-ci@npm:^3.0.0": - version: 3.0.1 - resolution: "@types/is-ci@npm:3.0.1" - dependencies: - ci-info: ^3.1.0 - checksum: c5cce9ffcd2528ebc731570855d23f99e2589d094e20ac5c3d87c2e53a456c2e7002851bd3fec4e3c20cdd8a5b090d8a90194e108192d9494c4d130ff9b65bbb - languageName: node - linkType: hard - "@types/is-uuid@npm:1.0.2": version: 1.0.2 resolution: "@types/is-uuid@npm:1.0.2" @@ -7474,13 +7505,13 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:29.5.9": - version: 29.5.9 - resolution: "@types/jest@npm:29.5.9" +"@types/jest@npm:29.5.10": + version: 29.5.10 + resolution: "@types/jest@npm:29.5.10" dependencies: expect: ^29.0.0 pretty-format: ^29.0.0 - checksum: 02245cff5f5b5ef46cc8c28acc516674aa167f2398bc4042752db7c763555928c5d62d56b3510eb8a2e5e25eea921f34866ef1c5515edfe16699b4a1e99ed4e1 + checksum: ef385905787db528de9b6beb2688865c0bb276e64256ed60b9a1a6ffc0b75737456cb5e27e952a3241c5845b6a1da487470010dd30f3ca59c8581624c564a823 languageName: node linkType: hard @@ -7635,12 +7666,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:20.9.3": - version: 20.9.3 - resolution: "@types/node@npm:20.9.3" +"@types/node@npm:20.10.3": + version: 20.10.3 + resolution: "@types/node@npm:20.10.3" dependencies: undici-types: ~5.26.4 - checksum: 0cfbfd2a8bd18acc75aa4d7685c7dcf56344f48addd4041d306dc194f3132f8014d56fd49fcb26bcdf400b883f9527e5e2beaf52dfce029cef15c69b8ed2e72a + checksum: 34a329494f0ea239af05eeb6f00f396963725b3eb9a2f79c5e6a6d37e823f2ab85e1079c2ee56723a37d8b89e7bbe2bd050c97144e5bb06dab93fd1cace65c97 languageName: node linkType: hard @@ -7714,12 +7745,12 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:18.2.15": - version: 18.2.15 - resolution: "@types/react-dom@npm:18.2.15" +"@types/react-dom@npm:18.2.17": + version: 18.2.17 + resolution: "@types/react-dom@npm:18.2.17" dependencies: "@types/react": "*" - checksum: 8e9631600c21ff561328e38a951d1991b3b3b20f538af4c0efbd1327c883a5573a63f50e1b945c34fa51b114b30e1ca5e62317bd54f21e063d6697b4be843a03 + checksum: 7a4e704ed4be6e0c3ccd8a22ff69386fe548304bf4db090513f42e059ff4c65f7a427790320051524d6578a2e4c9667bb7a80a4c989b72361c019fbe851d9385 languageName: node linkType: hard @@ -7773,10 +7804,10 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:7.5.5": - version: 7.5.5 - resolution: "@types/semver@npm:7.5.5" - checksum: 533e6c93d1262d65f449423d94a445f7f3db0672e7429f21b6a1636d6051dbab3a2989ddcda9b79c69bb37830931d09fc958a65305a891357f5cea3257c297f5 +"@types/semver@npm:7.5.6": + version: 7.5.6 + resolution: "@types/semver@npm:7.5.6" + checksum: 563a0120ec0efcc326567db2ed920d5d98346f3638b6324ea6b50222b96f02a8add3c51a916b6897b51523aad8ac227d21d3dcf8913559f1bfc6c15b14d23037 languageName: node linkType: hard @@ -7900,12 +7931,12 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:8.5.9": - version: 8.5.9 - resolution: "@types/ws@npm:8.5.9" +"@types/ws@npm:8.5.10": + version: 8.5.10 + resolution: "@types/ws@npm:8.5.10" dependencies: "@types/node": "*" - checksum: 83f436b731d2cdc49a45ced31a0a65cdd2e39c24d7b882776c26efa190dad6553e266d624c7a7089f36ad3ed471e02e729f3219282c80689b435f665df4a2b0b + checksum: 3ec416ea2be24042ebd677932a462cf16d2080393d8d7d0b1b3f5d6eaa4a7387aaf0eefb99193c0bfd29444857cf2e0c3ac89899e130550dc6c14ada8a46d25e languageName: node linkType: hard @@ -7934,15 +7965,15 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:6.12.0": - version: 6.12.0 - resolution: "@typescript-eslint/eslint-plugin@npm:6.12.0" +"@typescript-eslint/eslint-plugin@npm:6.13.1": + version: 6.13.1 + resolution: "@typescript-eslint/eslint-plugin@npm:6.13.1" dependencies: "@eslint-community/regexpp": ^4.5.1 - "@typescript-eslint/scope-manager": 6.12.0 - "@typescript-eslint/type-utils": 6.12.0 - "@typescript-eslint/utils": 6.12.0 - "@typescript-eslint/visitor-keys": 6.12.0 + "@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 debug: ^4.3.4 graphemer: ^1.4.0 ignore: ^5.2.4 @@ -7955,25 +7986,25 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: a791ebe432a6cac50a15c9e98502b62e874de0c7e35fd320b9bdca21afd4ae88c88cff45ee50a95362da14e98965d946e57b15965f5522f1153568a3fe45db8a + checksum: 568093d76c200a8502047d74f29300110a59b9f2a5cbf995a6cbe419c803a7ec22220e9592a884401d2dde72c79346b4cc0ee393e7b422924ad4a8a2040af3b0 languageName: node linkType: hard -"@typescript-eslint/parser@npm:6.12.0": - version: 6.12.0 - resolution: "@typescript-eslint/parser@npm:6.12.0" +"@typescript-eslint/parser@npm:6.13.1": + version: 6.13.1 + resolution: "@typescript-eslint/parser@npm:6.13.1" dependencies: - "@typescript-eslint/scope-manager": 6.12.0 - "@typescript-eslint/types": 6.12.0 - "@typescript-eslint/typescript-estree": 6.12.0 - "@typescript-eslint/visitor-keys": 6.12.0 + "@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 debug: ^4.3.4 peerDependencies: eslint: ^7.0.0 || ^8.0.0 peerDependenciesMeta: typescript: optional: true - checksum: 92923b7ee61f52d6b74f515640fe6bbb6b0a922d20dabeb6b59bc73f3c132bf750a2b706bb40fbe6d233c6ecc1abe905c99aa062280bb78e5724334f5b6c4ac5 + checksum: 58b7fef6f2d02c8f4737f9908a8d335a20bee20dba648233a69f28e7b39237791d2b9fbb818e628dcc053ddf16507b161ace7f1139e093d72365f1270c426de3 languageName: node linkType: hard @@ -7987,22 +8018,22 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:6.12.0": - version: 6.12.0 - resolution: "@typescript-eslint/scope-manager@npm:6.12.0" +"@typescript-eslint/scope-manager@npm:6.13.1": + version: 6.13.1 + resolution: "@typescript-eslint/scope-manager@npm:6.13.1" dependencies: - "@typescript-eslint/types": 6.12.0 - "@typescript-eslint/visitor-keys": 6.12.0 - checksum: 4cc4eb1bcd04ba7b0a1de4284521cde5f3f25f2530f78dfcb3f098396b142fd30a45f615a87dc7a3adddbd131a6255cb12b1df19aacff71a3f766992ddef183f + "@typescript-eslint/types": 6.13.1 + "@typescript-eslint/visitor-keys": 6.13.1 + checksum: 109a213f82719e10f8c6a0168f2e105dc1369c7e0c075c1f30af137030fc866a3a585a77ff78a9a3538afc213061c8aedbb4462a91f26cbd90eefbab8b89ea10 languageName: node linkType: hard -"@typescript-eslint/type-utils@npm:6.12.0": - version: 6.12.0 - resolution: "@typescript-eslint/type-utils@npm:6.12.0" +"@typescript-eslint/type-utils@npm:6.13.1": + version: 6.13.1 + resolution: "@typescript-eslint/type-utils@npm:6.13.1" dependencies: - "@typescript-eslint/typescript-estree": 6.12.0 - "@typescript-eslint/utils": 6.12.0 + "@typescript-eslint/typescript-estree": 6.13.1 + "@typescript-eslint/utils": 6.13.1 debug: ^4.3.4 ts-api-utils: ^1.0.1 peerDependencies: @@ -8010,7 +8041,7 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: c345c45f1262eee4b9f6960a59b3aba960643d0004094a3d8fb9682ab79af2fae864695029246dc9e0d4fdb2f3d017a56b7dc034e551d263deba75c2ef048d39 + checksum: e39d28dd2f3b47a26b4f6aa2c7a301bdd769ce9148d734be93441a813c3d1111eba1d655677355bba5519f3d4dbe93e4ff4e46830216b0302df0070bf7a80057 languageName: node linkType: hard @@ -8021,10 +8052,10 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:6.12.0": - version: 6.12.0 - resolution: "@typescript-eslint/types@npm:6.12.0" - checksum: d3b40f9d400f6455ce5ae610651597c9e9ec85d46ca6d3c1025597a76305c557ebc5b88340ec6db0e694c9c79f1299d375b87a1a5b9314b22231dbbb5ce54695 +"@typescript-eslint/types@npm:6.13.1": + version: 6.13.1 + resolution: "@typescript-eslint/types@npm:6.13.1" + checksum: bb1d52f1646bab9acd3ec874567ffbaaaf7fe4a5f79845bdacbfea46d15698e58d45797da05b08c23f9496a17229b7f2c1363d000fd89ce4e79874fd57ba1d4a languageName: node linkType: hard @@ -8046,12 +8077,12 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:6.12.0": - version: 6.12.0 - resolution: "@typescript-eslint/typescript-estree@npm:6.12.0" +"@typescript-eslint/typescript-estree@npm:6.13.1": + version: 6.13.1 + resolution: "@typescript-eslint/typescript-estree@npm:6.13.1" dependencies: - "@typescript-eslint/types": 6.12.0 - "@typescript-eslint/visitor-keys": 6.12.0 + "@typescript-eslint/types": 6.13.1 + "@typescript-eslint/visitor-keys": 6.13.1 debug: ^4.3.4 globby: ^11.1.0 is-glob: ^4.0.3 @@ -8060,24 +8091,24 @@ __metadata: peerDependenciesMeta: typescript: optional: true - checksum: 943f7ff2e164d812f6ae0a2d5096836aff00b1fda39937b03f126f266f03f3655794f5fc4643b49b71c312126d9422dfd764744bd1ba41ee6821a5bac1511aa2 + checksum: 09aa0f5cbd60e84df4f58f3d479be352549600b24dbefe75c686ea89252526c52c1c06ce1ae56c0405dd7337002e741c2ba02b71fb1caa3b94a740a70fcc8699 languageName: node linkType: hard -"@typescript-eslint/utils@npm:6.12.0": - version: 6.12.0 - resolution: "@typescript-eslint/utils@npm:6.12.0" +"@typescript-eslint/utils@npm:6.13.1": + version: 6.13.1 + resolution: "@typescript-eslint/utils@npm:6.13.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.12.0 - "@typescript-eslint/types": 6.12.0 - "@typescript-eslint/typescript-estree": 6.12.0 + "@typescript-eslint/scope-manager": 6.13.1 + "@typescript-eslint/types": 6.13.1 + "@typescript-eslint/typescript-estree": 6.13.1 semver: ^7.5.4 peerDependencies: eslint: ^7.0.0 || ^8.0.0 - checksum: dad05bd0e4db7a88c2716f9ee83c7c28c30d71e57392e58dc0db66b5f5c4c86b9db14142c6a1a82cf1650da294d31980c56a118015d3a2a645acb8b8a5ebc315 + checksum: 14f64840869c8755af4d287cfc74abc424dc139559e87ca1a8b0e850f4fa56311d99dfb61a43dd4433eae5914be12b4b3390e55de1f236dce6701830d17e31c9 languageName: node linkType: hard @@ -8109,13 +8140,13 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:6.12.0": - version: 6.12.0 - resolution: "@typescript-eslint/visitor-keys@npm:6.12.0" +"@typescript-eslint/visitor-keys@npm:6.13.1": + version: 6.13.1 + resolution: "@typescript-eslint/visitor-keys@npm:6.13.1" dependencies: - "@typescript-eslint/types": 6.12.0 + "@typescript-eslint/types": 6.13.1 eslint-visitor-keys: ^3.4.1 - checksum: 3d8dc74ae748a95fe60b48dbaecca8d9c0c8df344d8034e3843057251fba24f06a3d29dbb9f525c9540b538d8c24221d3cf119ac483e9de38149a978051c72f3 + checksum: d15d362203a2fe995ea62a59d5b44c15c8fb1fb30ff59dd1542a980f75b3b62035303dfb781d83709921613f6ac8cc5bf57b70f6e20d820aec8b7911f07152e9 languageName: node linkType: hard @@ -8683,12 +8714,12 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^5.0.0": - version: 5.0.0 - resolution: "ansi-escapes@npm:5.0.0" +"ansi-escapes@npm:^6.2.0": + version: 6.2.0 + resolution: "ansi-escapes@npm:6.2.0" dependencies: - type-fest: ^1.0.2 - checksum: d4b5eb8207df38367945f5dd2ef41e08c28edc192dc766ef18af6b53736682f49d8bfcfa4e4d6ecbc2e2f97c258fda084fb29a9e43b69170b71090f771afccac + type-fest: ^3.0.0 + checksum: f0bc667d5f1ededc3ea89b73c34f0cba95473525b07e1290ddfd3fc868c94614e95f3549f5c4fd0c05424af7d3fd298101fb3d9a52a597d3782508b340783bd7 languageName: node linkType: hard @@ -8768,7 +8799,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0": +"ansi-styles@npm:^6.0.0, ansi-styles@npm:^6.1.0, ansi-styles@npm:^6.2.1": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" checksum: ef940f2f0ced1a6347398da88a91da7930c33ecac3c77b72c5905f8b8fe402c52e6fde304ff5347f616e27a742da3f1dc76de98f6866c69251ad0b07a66776d9 @@ -8829,10 +8860,10 @@ __metadata: version: 0.0.0-use.local resolution: "apollo-federation-subgraph-compatibility@workspace:packages/apollo-federation-subgraph-compatibility" dependencies: - "@apollo/federation-subgraph-compatibility": 2.0.1 + "@apollo/federation-subgraph-compatibility": 2.1.0 "@apollo/server": ^4.7.0 "@graphql-tools/wrap": ^10.0.0 - "@neo4j/graphql": ^4.4.3 + "@neo4j/graphql": ^4.4.4 fork-ts-checker-webpack-plugin: 9.0.2 graphql: 16.8.1 graphql-tag: ^2.12.6 @@ -10439,13 +10470,20 @@ __metadata: languageName: node linkType: hard -"ci-info@npm:^3.1.0, ci-info@npm:^3.2.0": +"ci-info@npm:^3.2.0": version: 3.8.0 resolution: "ci-info@npm:3.8.0" checksum: d0a4d3160497cae54294974a7246202244fff031b0a6ea20dd57b10ec510aa17399c41a1b0982142c105f3255aff2173e5c0dd7302ee1b2f28ba3debda375098 languageName: node linkType: hard +"ci-info@npm:^3.7.0": + version: 3.9.0 + resolution: "ci-info@npm:3.9.0" + checksum: 6b19dc9b2966d1f8c2041a838217299718f15d6c4b63ae36e4674edd2bee48f780e94761286a56aa59eb305a85fbea4ddffb7630ec063e7ec7e7e5ad42549a87 + languageName: node + linkType: hard + "cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3": version: 1.0.4 resolution: "cipher-base@npm:1.0.4" @@ -10559,13 +10597,13 @@ __metadata: languageName: node linkType: hard -"cli-truncate@npm:^3.1.0": - version: 3.1.0 - resolution: "cli-truncate@npm:3.1.0" +"cli-truncate@npm:^4.0.0": + version: 4.0.0 + resolution: "cli-truncate@npm:4.0.0" dependencies: slice-ansi: ^5.0.0 - string-width: ^5.0.0 - checksum: c3243e41974445691c63f8b405df1d5a24049dc33d324fe448dc572e561a7b772ae982692900b1a5960901cc4fc7def25a629b9c69a4208ee89d12ab3332617a + string-width: ^7.0.0 + checksum: d5149175fd25ca985731bdeec46a55ec237475cf74c1a5e103baea696aceb45e372ac4acbaabf1316f06bd62e348123060f8191ffadfeedebd2a70a2a7fb199d languageName: node linkType: hard @@ -11372,6 +11410,15 @@ __metadata: languageName: node linkType: hard +"cross-inspect@npm:1.0.0": + version: 1.0.0 + resolution: "cross-inspect@npm:1.0.0" + dependencies: + tslib: ^2.4.0 + checksum: 975c81799549627027254eb70f1c349cefb14435d580bea6f351f510c839dcb1a9288983407bac2ad317e6eff29cf1e99299606da21f404562bfa64cec502239 + languageName: node + linkType: hard + "cross-spawn@npm:^5.1.0": version: 5.1.0 resolution: "cross-spawn@npm:5.1.0" @@ -12692,6 +12739,13 @@ __metadata: languageName: node linkType: hard +"emoji-regex@npm:^10.3.0": + version: 10.3.0 + resolution: "emoji-regex@npm:10.3.0" + checksum: 5da48edfeb9462fb1ae5495cff2d79129974c696853fb0ce952cbf560f29a2756825433bf51cfd5157ec7b9f93f46f31d712e896d63e3d8ac9c3832bdb45ab73 + languageName: node + linkType: hard + "emoji-regex@npm:^7.0.1": version: 7.0.3 resolution: "emoji-regex@npm:7.0.3" @@ -13122,14 +13176,14 @@ __metadata: languageName: node linkType: hard -"eslint-config-prettier@npm:9.0.0": - version: 9.0.0 - resolution: "eslint-config-prettier@npm:9.0.0" +"eslint-config-prettier@npm:9.1.0": + version: 9.1.0 + resolution: "eslint-config-prettier@npm:9.1.0" peerDependencies: eslint: ">=7.0.0" bin: eslint-config-prettier: bin/cli.js - checksum: 362e991b6cb343f79362bada2d97c202e5303e6865888918a7445c555fb75e4c078b01278e90be98aa98ae22f8597d8e93d48314bec6824f540f7efcab3ce451 + checksum: 9229b768c879f500ee54ca05925f31b0c0bafff3d9f5521f98ff05127356de78c81deb9365c86a5ec4efa990cb72b74df8612ae15965b14136044c73e1f6a907 languageName: node linkType: hard @@ -13333,14 +13387,14 @@ __metadata: languageName: node linkType: hard -"eslint@npm:8.54.0": - version: 8.54.0 - resolution: "eslint@npm:8.54.0" +"eslint@npm:8.55.0": + version: 8.55.0 + resolution: "eslint@npm:8.55.0" dependencies: "@eslint-community/eslint-utils": ^4.2.0 "@eslint-community/regexpp": ^4.6.1 - "@eslint/eslintrc": ^2.1.3 - "@eslint/js": 8.54.0 + "@eslint/eslintrc": ^2.1.4 + "@eslint/js": 8.55.0 "@humanwhocodes/config-array": ^0.11.13 "@humanwhocodes/module-importer": ^1.0.1 "@nodelib/fs.walk": ^1.2.8 @@ -13377,7 +13431,7 @@ __metadata: text-table: ^0.2.0 bin: eslint: bin/eslint.js - checksum: 7e876e9da2a18a017271cf3733d05a3dfbbe469272d75753408c6ea5b1646c71c6bb18cb91e10ca930144c32c1ce3701e222f1ae6784a3975a69f8f8aa68e49f + checksum: 83f82a604559dc1faae79d28fdf3dfc9e592ca221052e2ea516e1b379b37e77e4597705a16880e2f5ece4f79087c1dd13fd7f6e9746f794a401175519db18b41 languageName: node linkType: hard @@ -14504,6 +14558,13 @@ __metadata: languageName: node linkType: hard +"get-east-asian-width@npm:^1.0.0": + version: 1.2.0 + resolution: "get-east-asian-width@npm:1.2.0" + checksum: ea55f4d4a42c4b00d3d9be3111bc17eb0161f60ed23fc257c1390323bb780a592d7a8bdd550260fd4627dabee9a118cdfa3475ae54edca35ebcd3bdae04179e3 + languageName: node + linkType: hard + "get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.0, get-intrinsic@npm:^1.2.1": version: 1.2.1 resolution: "get-intrinsic@npm:1.2.1" @@ -16216,17 +16277,6 @@ __metadata: languageName: node linkType: hard -"is-ci@npm:^3.0.1": - version: 3.0.1 - resolution: "is-ci@npm:3.0.1" - dependencies: - ci-info: ^3.2.0 - bin: - is-ci: bin.js - checksum: 192c66dc7826d58f803ecae624860dccf1899fc1f3ac5505284c0a5cf5f889046ffeb958fa651e5725d5705c5bcb14f055b79150ea5fcad7456a9569de60260e - languageName: node - linkType: hard - "is-core-module@npm:^2.11.0, is-core-module@npm:^2.13.0, is-core-module@npm:^2.5.0, is-core-module@npm:^2.9.0": version: 2.13.0 resolution: "is-core-module@npm:2.13.0" @@ -16316,6 +16366,15 @@ __metadata: languageName: node linkType: hard +"is-fullwidth-code-point@npm:^5.0.0": + version: 5.0.0 + resolution: "is-fullwidth-code-point@npm:5.0.0" + dependencies: + get-east-asian-width: ^1.0.0 + checksum: 8dfb2d2831b9e87983c136f5c335cd9d14c1402973e357a8ff057904612ed84b8cba196319fabedf9aefe4639e14fe3afe9d9966d1d006ebeb40fe1fed4babe5 + languageName: node + linkType: hard + "is-generator-fn@npm:^2.0.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" @@ -18043,7 +18102,14 @@ __metadata: languageName: node linkType: hard -"lilconfig@npm:2.1.0, lilconfig@npm:^2.0.5, lilconfig@npm:^2.1.0": +"lilconfig@npm:3.0.0": + version: 3.0.0 + resolution: "lilconfig@npm:3.0.0" + checksum: a155f1cd24d324ab20dd6974db9ebcf3fb6f2b60175f7c052d917ff8a746b590bc1ee550f6fc3cb1e8716c8b58304e22fe2193febebc0cf16fa86d85e6f896c5 + languageName: node + linkType: hard + +"lilconfig@npm:^2.0.5, lilconfig@npm:^2.1.0": version: 2.1.0 resolution: "lilconfig@npm:2.1.0" checksum: 8549bb352b8192375fed4a74694cd61ad293904eee33f9d4866c2192865c44c4eb35d10782966242634e0cbc1e91fe62b1247f148dc5514918e3a966da7ea117 @@ -18082,23 +18148,23 @@ __metadata: languageName: node linkType: hard -"lint-staged@npm:15.1.0": - version: 15.1.0 - resolution: "lint-staged@npm:15.1.0" +"lint-staged@npm:15.2.0": + version: 15.2.0 + resolution: "lint-staged@npm:15.2.0" dependencies: chalk: 5.3.0 commander: 11.1.0 debug: 4.3.4 execa: 8.0.1 - lilconfig: 2.1.0 - listr2: 7.0.2 + lilconfig: 3.0.0 + listr2: 8.0.0 micromatch: 4.0.5 pidtree: 0.6.0 string-argv: 0.3.2 yaml: 2.3.4 bin: lint-staged: bin/lint-staged.js - checksum: e99bdedb32d20fa22c0d0798ecf014fd00ac9cce1158373d7caf47855c0b9b4c20d228417677a05ea81f6941f957ae9347dccb3846a48bc1fdd0cdeed2ccf0ef + checksum: 4fb178b8d3ff454f7874697dfbd41017630f61a06296d12ac9dfd578d078c70aff7108b67fab38af94896ef2740a1e7541c1512d0d3c688ed90e6c3af3530f0d languageName: node linkType: hard @@ -18151,17 +18217,17 @@ __metadata: languageName: node linkType: hard -"listr2@npm:7.0.2": - version: 7.0.2 - resolution: "listr2@npm:7.0.2" +"listr2@npm:8.0.0": + version: 8.0.0 + resolution: "listr2@npm:8.0.0" dependencies: - cli-truncate: ^3.1.0 + cli-truncate: ^4.0.0 colorette: ^2.0.20 eventemitter3: ^5.0.1 - log-update: ^5.0.1 + log-update: ^6.0.0 rfdc: ^1.3.0 - wrap-ansi: ^8.1.0 - checksum: 1734c6b9367ceeb09bf372427930a4586b3727097373408f2f840896b9333cc80e53a1a696771a83a7d4d9ada46229843f3052b87f3b0b58c20e9451362c2dd3 + wrap-ansi: ^9.0.0 + checksum: 5cb110a710d14488c71d2207fc5141256abb1f21cbe5ebc12177ae640f94e040a1ef8c031b70ff9f24c4a8fa57c0825a54b534e52bdfaffc122a81082faae8ed languageName: node linkType: hard @@ -18467,16 +18533,16 @@ __metadata: languageName: node linkType: hard -"log-update@npm:^5.0.1": - version: 5.0.1 - resolution: "log-update@npm:5.0.1" +"log-update@npm:^6.0.0": + version: 6.0.0 + resolution: "log-update@npm:6.0.0" dependencies: - ansi-escapes: ^5.0.0 + ansi-escapes: ^6.2.0 cli-cursor: ^4.0.0 - slice-ansi: ^5.0.0 - strip-ansi: ^7.0.1 - wrap-ansi: ^8.0.1 - checksum: 2c6b47dcce6f9233df6d232a37d9834cb3657a0749ef6398f1706118de74c55f158587d4128c225297ea66803f35c5ac3db4f3f617046d817233c45eedc32ef1 + slice-ansi: ^7.0.0 + strip-ansi: ^7.1.0 + wrap-ansi: ^9.0.0 + checksum: 8803ceba2fb28626951b85de598c8d5a4f5e39f1f767cc54fd925412cc7780ba89ce1dbec24dc96fa46f89d226e1ae984534aa729dc9c9b734e36bb805428ffa languageName: node linkType: hard @@ -19486,6 +19552,15 @@ __metadata: languageName: node linkType: hard +"mustache@npm:^4.2.0": + version: 4.2.0 + resolution: "mustache@npm:4.2.0" + bin: + mustache: bin/mustache + checksum: 928fcb63e3aa44a562bfe9b59ba202cccbe40a46da50be6f0dd831b495be1dd7e38ca4657f0ecab2c1a89dc7bccba0885eab7ee7c1b215830da765758c7e0506 + languageName: node + linkType: hard + "mute-stream@npm:0.0.7": version: 0.0.7 resolution: "mute-stream@npm:0.0.7" @@ -19589,6 +19664,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: d36c427e530713e4ac6567d488b489a36582ef89da1d6d4e3b87eded11eb10d7042a877958c6f104929809b2ab0bafa17652b076cdf84324aa75b30b722204f2 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -19741,14 +19825,14 @@ __metadata: languageName: node linkType: hard -"neo4j-driver-bolt-connection@npm:5.14.0": - version: 5.14.0 - resolution: "neo4j-driver-bolt-connection@npm:5.14.0" +"neo4j-driver-bolt-connection@npm:5.15.0": + version: 5.15.0 + resolution: "neo4j-driver-bolt-connection@npm:5.15.0" dependencies: buffer: ^6.0.3 - neo4j-driver-core: 5.14.0 + neo4j-driver-core: 5.15.0 string_decoder: ^1.3.0 - checksum: 1f7fe0331ad6db1fedc7b9c0863f97579c65475a3baf701a33c3e311a1556924048fc9f63635070c418c278142f5890d78f8867015cd8ce6d19e4e89bfc3a721 + checksum: 5e36d6eab0130df8d15e71ad85ca36697292e32ea4195ec7f33a547675942f3148a30c5aa99fc93e830d4b3fb771d50d05f98a12f1134bd7f06d2c09a3ca8d36 languageName: node linkType: hard @@ -19759,21 +19843,21 @@ __metadata: languageName: node linkType: hard -"neo4j-driver-core@npm:5.14.0": - version: 5.14.0 - resolution: "neo4j-driver-core@npm:5.14.0" - checksum: bc676f698f817d12e34b2936ade6993c626e0e345c86a729895232e129f50f7c2358ed36aec282bb256afed3ee0d1902a3a953d30d0aeaed338d93af25214612 +"neo4j-driver-core@npm:5.15.0": + version: 5.15.0 + resolution: "neo4j-driver-core@npm:5.15.0" + checksum: c13ba8cf0a68ee5de7d3855aa1e5b8e724c8f735ab5514e8dd8cfaee71d808faf38b9511666d223005a10340a1929a7d90074c7596e7f25daa47bb77fcbd0a96 languageName: node linkType: hard -"neo4j-driver@npm:5.14.0": - version: 5.14.0 - resolution: "neo4j-driver@npm:5.14.0" +"neo4j-driver@npm:5.15.0": + version: 5.15.0 + resolution: "neo4j-driver@npm:5.15.0" dependencies: - neo4j-driver-bolt-connection: 5.14.0 - neo4j-driver-core: 5.14.0 + neo4j-driver-bolt-connection: 5.15.0 + neo4j-driver-core: 5.15.0 rxjs: ^7.8.1 - checksum: 6982004944901327b8f7cccc0939c935d33f1c7a63c1783c64f5af2ff5f1fe45cbb702abd0a507c93831232eafae39236affada4e8916f99318d10dfcfe4e35d + checksum: 9e80d00719c1ec24bddfd9f221690db992e4102cb511f7809ae6f77f3fbc555b6420f205b24a6132cd91784356323c69eefc3731ee40c919faaaf791b03eafbb languageName: node linkType: hard @@ -19792,15 +19876,15 @@ __metadata: version: 0.0.0-use.local resolution: "neo4j-graphql@workspace:." dependencies: - "@changesets/changelog-github": 0.4.8 - "@changesets/cli": 2.26.2 + "@changesets/changelog-github": 0.5.0 + "@changesets/cli": 2.27.1 "@tsconfig/node16": 1.0.4 - "@typescript-eslint/eslint-plugin": 6.12.0 - "@typescript-eslint/parser": 6.12.0 + "@typescript-eslint/eslint-plugin": 6.13.1 + "@typescript-eslint/parser": 6.13.1 concurrently: 8.2.2 dotenv: 16.3.1 - eslint: 8.54.0 - eslint-config-prettier: 9.0.0 + eslint: 8.55.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 @@ -19812,8 +19896,8 @@ __metadata: graphql: 16.8.1 husky: 8.0.3 jest: 29.7.0 - lint-staged: 15.1.0 - neo4j-driver: 5.14.0 + lint-staged: 15.2.0 + neo4j-driver: 5.15.0 npm-run-all: 4.1.5 prettier: 2.8.8 set-tz: 0.2.0 @@ -19871,14 +19955,14 @@ __metadata: languageName: node linkType: hard -"nock@npm:13.3.8": - version: 13.3.8 - resolution: "nock@npm:13.3.8" +"nock@npm:13.4.0": + version: 13.4.0 + resolution: "nock@npm:13.4.0" dependencies: debug: ^4.1.0 json-stringify-safe: ^5.0.1 propagate: ^2.0.0 - checksum: 98f7d9d1c6b4fad560d7f1033705f9a0318e288060c10e36973d1798d6c824fee1f23a9ecbb1118bf70068f58bb04eaa50c5d046f5cf0ceaf4a2dc76fe7a82b2 + checksum: 30c3751854f9c412df5f99e01eeaef25b2583d3cae80b8c46524acb39d8b7fa61043603472ad94a3adc4b7d1e0f3098e6bb06e787734cbfbde2751891115b311 languageName: node linkType: hard @@ -21320,27 +21404,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.40.0": - version: 1.40.0 - resolution: "playwright-core@npm:1.40.0" +"playwright-core@npm:1.40.1": + version: 1.40.1 + resolution: "playwright-core@npm:1.40.1" bin: playwright-core: cli.js - checksum: 57de5c91a4c404b120ed2af8541b21cdedcbc4f27477341157666d356bbee3b3fab8e61d020f0f450708fa2e8f6dc244b9224cb1985d5426e609cebed15af095 + checksum: 84d92fb9b86e3c225b16b6886bf858eb5059b4e60fa1205ff23336e56a06dcb2eac62650992dede72f406c8e70a7b6a5303e511f9b4bc0b85022ede356a01ee0 languageName: node linkType: hard -"playwright@npm:1.40.0": - version: 1.40.0 - resolution: "playwright@npm:1.40.0" +"playwright@npm:1.40.1": + version: 1.40.1 + resolution: "playwright@npm:1.40.1" dependencies: fsevents: 2.3.2 - playwright-core: 1.40.0 + playwright-core: 1.40.1 dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 7ba49e5376a6cfd1d32048dbdb2fd38e09182aa2e4619fdb23d3e6530fa6987f2f3fd34ad1d9d906fb4ec2da69ee7536eeb881982d60750fde809183caa607fc + checksum: 9e36791c1b4a649c104aa365fdd9d049924eeb518c5967c0e921aa38b9b00994aa6ee54784d6c2af194b3b494b6f69772673081ef53c6c4a4b2065af9955c4ba languageName: node linkType: hard @@ -21603,14 +21687,14 @@ __metadata: languageName: node linkType: hard -"postcss@npm:8.4.31": - version: 8.4.31 - resolution: "postcss@npm:8.4.31" +"postcss@npm:8.4.32": + version: 8.4.32 + resolution: "postcss@npm:8.4.32" dependencies: - nanoid: ^3.3.6 + nanoid: ^3.3.7 picocolors: ^1.0.0 source-map-js: ^1.0.2 - checksum: 1d8611341b073143ad90486fcdfeab49edd243377b1f51834dc4f6d028e82ce5190e4f11bb2633276864503654fb7cab28e67abdc0fbf9d1f88cad4a0ff0beea + checksum: 220d9d0bf5d65be7ed31006c523bfb11619461d296245c1231831f90150aeb4a31eab9983ac9c5c89759a3ca8b60b3e0d098574964e1691673c3ce5c494305ae languageName: node linkType: hard @@ -23718,6 +23802,16 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^7.0.0": + version: 7.1.0 + resolution: "slice-ansi@npm:7.1.0" + dependencies: + ansi-styles: ^6.2.1 + is-fullwidth-code-point: ^5.0.0 + checksum: 10313dd3cf7a2e4b265f527b1684c7c568210b09743fd1bd74f2194715ed13ffba653dc93a5fa79e3b1711518b8990a732cb7143aa01ddafe626e99dfa6474b2 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -24304,7 +24398,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^5.0.0, string-width@npm:^5.0.1, string-width@npm:^5.1.2": +"string-width@npm:^5.0.1, string-width@npm:^5.1.2": version: 5.1.2 resolution: "string-width@npm:5.1.2" dependencies: @@ -24315,6 +24409,17 @@ __metadata: languageName: node linkType: hard +"string-width@npm:^7.0.0": + version: 7.0.0 + resolution: "string-width@npm:7.0.0" + dependencies: + emoji-regex: ^10.3.0 + get-east-asian-width: ^1.0.0 + strip-ansi: ^7.1.0 + checksum: bc0de5700a2690895169fce447ec4ed44bc62de80312c2093d5606bfd48319bb88e48a99e97f269dff2bc9577448b91c26b3804c16e7d9b389699795e4655c3b + languageName: node + linkType: hard + "string.prototype.matchall@npm:^4.0.8": version: 4.0.10 resolution: "string.prototype.matchall@npm:4.0.10" @@ -24437,7 +24542,7 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^7.0.1": +"strip-ansi@npm:^7.0.1, strip-ansi@npm:^7.1.0": version: 7.1.0 resolution: "strip-ansi@npm:7.1.0" dependencies: @@ -25534,13 +25639,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^1.0.2": - version: 1.4.0 - resolution: "type-fest@npm:1.4.0" - checksum: b011c3388665b097ae6a109a437a04d6f61d81b7357f74cbcb02246f2f5bd72b888ae33631b99871388122ba0a87f4ff1c94078e7119ff22c70e52c0ff828201 - languageName: node - linkType: hard - "type-fest@npm:^2.14.0": version: 2.19.0 resolution: "type-fest@npm:2.19.0" @@ -25548,6 +25646,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^3.0.0": + version: 3.13.1 + resolution: "type-fest@npm:3.13.1" + checksum: c06b0901d54391dc46de3802375f5579868949d71f93b425ce564e19a428a0d411ae8d8cb0e300d330071d86152c3ea86e744c3f2860a42a79585b6ec2fdae8e + languageName: node + linkType: hard + "type-is@npm:^1.6.16, type-is@npm:~1.6.18": version: 1.6.18 resolution: "type-is@npm:1.6.18" @@ -26752,7 +26857,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^8.0.1, wrap-ansi@npm:^8.1.0": +"wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" dependencies: @@ -26763,6 +26868,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi@npm:^9.0.0": + version: 9.0.0 + resolution: "wrap-ansi@npm:9.0.0" + dependencies: + ansi-styles: ^6.2.1 + string-width: ^7.0.0 + strip-ansi: ^7.1.0 + checksum: b2d43b76b3d8dcbdd64768165e548aad3e54e1cae4ecd31bac9966faaa7cf0b0345677ad6879db10ba58eb446ba8fa44fb82b4951872fd397f096712467a809f + languageName: node + linkType: hard + "wrappy@npm:1": version: 1.0.2 resolution: "wrappy@npm:1.0.2" @@ -27053,9 +27169,9 @@ __metadata: languageName: unknown linkType: soft -"zustand@npm:4.4.6": - version: 4.4.6 - resolution: "zustand@npm:4.4.6" +"zustand@npm:4.4.7": + version: 4.4.7 + resolution: "zustand@npm:4.4.7" dependencies: use-sync-external-store: 1.2.0 peerDependencies: @@ -27069,6 +27185,6 @@ __metadata: optional: true react: optional: true - checksum: da7b00cc6dbe5cf5fc2e3fbca745317da4bbaf53bf4a6909bbd3e335242704df9689027f613461aff07eb5f672d5570bc1a2ef99d0ad7bc868920a3b331613d4 + checksum: 9aeb6cc86162296c1dac504b8906ff952252c129fb3f05cc8926a7f5c9d7fbe098571d5161b3efe3347c0ee1d80197f787b768c7522721864f7323c28766717e languageName: node linkType: hard