diff --git a/.commitlintrc b/.commitlintrc index 73d886db1db..ea24b734af1 100644 --- a/.commitlintrc +++ b/.commitlintrc @@ -6,20 +6,24 @@ 2, "always", [ + "laravel", "symfony", "doctrine", "metadata", "elasticsearch", "mongodb", "jsonld", + "httpcache", "hydra", + "httpcache", + "hal", "jsonapi", "graphql", "openapi", "parametervalidator", "serializer", "jsonschema", - "validation", + "validator", "state", "test" ] diff --git a/.gitattributes b/.gitattributes index 5988d06499a..a2afb552166 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,14 +1,18 @@ -/.editorconfig export-ignore -/.gitattributes export-ignore -/.github export-ignore -/.gitignore export-ignore -/.php_cs.dist export-ignore +*.sh export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.github export-ignore +.gitignore export-ignore +.php-cs-fixer.dist.php export-ignore +phpstan.neon.dist export-ignore +phpunit.xml.dist export-ignore +/.commitlintrc export-ignore /appveyor.yml export-ignore /behat.yml.dist export-ignore +/codecov.yml /docs export-ignore /features export-ignore -/phpstan.neon.dist export-ignore -/phpunit.xml.dist export-ignore +/package-lock.json export-ignore +/pmu.baseline /tests export-ignore -/update-js.sh export-ignore /yarn.lock export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6dbc09d70ba..4f667008dbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,28 +10,9 @@ concurrency: env: COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPOSER_ROOT_VERSION: "4.1.x-dev" jobs: - commitlint: - if: github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Run commitlint - run: | - commit=$(gh api \ - /repos/${{ github.repository }}/pulls/${{github.event.number}}/commits \ - | jq -r '.[0].commit.message' \ - | head -n 1) - # we can't use npx see https://github.com/conventional-changelog/commitlint/issues/613 - echo '{}' > package.json - npm install --no-fund --no-audit @commitlint/config-conventional @commitlint/cli - echo $commit | ./node_modules/.bin/commitlint -g .commitlintrc - architecture: name: Check components interdependencies runs-on: ubuntu-latest @@ -39,7 +20,7 @@ jobs: strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false steps: - name: Checkout @@ -61,8 +42,12 @@ jobs: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- + - run: composer validate - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - run: composer check-dependencies php-cs-fixer: @@ -95,7 +80,7 @@ jobs: strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false steps: - name: Checkout @@ -119,7 +104,9 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies run: | - composer update --no-interaction --no-progress --ansi + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Run container lint run: tests/Fixtures/app/console lint:container @@ -130,14 +117,16 @@ jobs: strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false env: APP_DEBUG: '1' # https://github.com/phpstan/phpstan-symfony/issues/37 - SYMFONY_PHPUNIT_VERSION: '9.5' steps: - name: Checkout uses: actions/checkout@v4 + # https://github.com/staabm/phpstan-todo-by#prerequisite + - name: Get tags + run: git fetch --tags origin - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -157,9 +146,9 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies run: | - composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Cache PHPStan results uses: actions/cache@v4 with: @@ -177,6 +166,15 @@ jobs: run: | ./vendor/bin/phpstan --version ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi + - name: Install Laravel + working-directory: 'src/Laravel' + run: | + composer global link ../../ --working-directory=$(pwd) + composer run-script build + - name: Run PHPStan analysis (laravel) + working-directory: 'src/Laravel' + run: | + ./vendor/bin/phpstan analyse --no-interaction --no-progress --ansi phpunit: name: PHPUnit (PHP ${{ matrix.php }}) @@ -185,15 +183,13 @@ jobs: strategy: matrix: php: - - '8.1' - '8.2' - '8.3' + - '8.4' include: - - php: '8.1' - coverage: true - php: '8.2' - coverage: true - php: '8.3' + - php: '8.4' coverage: true fail-fast: false steps: @@ -217,13 +213,14 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml ${{ matrix.coverage && '--coverage-clover build/logs/phpunit/clover.xml' || '' }} + run: vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml ${{ matrix.coverage && '--coverage-clover build/logs/phpunit/clover.xml' || '' }} - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 @@ -233,7 +230,7 @@ jobs: continue-on-error: true - name: Upload coverage results to Codecov if: matrix.coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: directory: build/logs/phpunit name: phpunit-php${{ matrix.php }} @@ -251,38 +248,35 @@ jobs: continue-on-error: true phpunit-components: - name: PHPUnit ${{ matrix.component }} (PHP ${{ matrix.php }}) + name: PHPUnit ${{ matrix.component }} (PHP ${{ matrix.php.version }} ${{ matrix.php.coverage && 'coverage' || '' }}${{ matrix.php.lowest && 'lowest' || '' }}) runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: php: - - '8.1' - - '8.2' - - '8.3' + - version: '8.2' + - version: '8.3' + - version: '8.4' + coverage: true + - version: '8.4' + lowest: true component: - api-platform/doctrine-common - api-platform/doctrine-orm - api-platform/doctrine-odm - api-platform/metadata + - api-platform/hydra + - api-platform/json-api - api-platform/json-schema - api-platform/elasticsearch - api-platform/openapi - api-platform/graphql - api-platform/http-cache - - api-platform/parameter-validator - api-platform/ramsey-uuid - api-platform/serializer - api-platform/state - api-platform/symfony - api-platform/validator - include: - - php: '8.1' - coverage: true - - php: '8.2' - coverage: true - - php: '8.3' - coverage: true fail-fast: false steps: - name: Checkout @@ -290,22 +284,28 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php }} + php-version: ${{ matrix.php.version }} tools: pecl, composer extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite, mongodb ini-values: memory_limit=-1 - - name: Run ${{ matrix.component }} install + - name: PMU + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + - name: Linking + if: ${{ !matrix.php.lowest }} run: | - composer update + composer global link . --permanent composer ${{matrix.component}} update - - name: PHP version tweaks - if: matrix.component == 'api-platform/metadata' && matrix.php != '8.1' - run: composer require symfony/type-info - working-directory: 'src/Metadata' + - name: Run ${{ matrix.component }} install + if: ${{ matrix.php.lowest }} + run: | + cd $(composer ${{matrix.component}} --cwd) + composer update${{ matrix.php.lowest && ' --prefer-lowest --prefer-source' || '' }} - name: Run ${{ matrix.component }} tests run: | mkdir -p /tmp/build/logs/phpunit - composer ${{matrix.component}} test --log-junit "/tmp/build/logs/phpunit/junit.xml" ${{ matrix.coverage && '--coverage-clover /tmp/build/logs/phpunit/clover.xml' || '' }} + composer ${{matrix.component}} test -- --log-junit "/tmp/build/logs/phpunit/junit.xml" ${{ matrix.php.coverage && '--coverage-clover /tmp/build/logs/phpunit/clover.xml' || '' }}${{ matrix.php.lowest && ' --ignore-baseline' || '' }} - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 @@ -315,7 +315,7 @@ jobs: continue-on-error: true - name: Upload coverage results to Codecov if: matrix.coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: directory: /tmp/build/logs/phpunit name: phpunit-php${{ matrix.php }} @@ -332,6 +332,56 @@ jobs: php-coveralls --coverage_clover=/tmp/build/logs/phpunit/clover.xml continue-on-error: true + phpunit-components-fail-deprecation: + name: PHPUnit no deprecations ${{ matrix.component }} (PHP ${{ matrix.php.version }} + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + php: + - version: '8.4' + component: + - api-platform/doctrine-common + - api-platform/doctrine-orm + - api-platform/doctrine-odm + - api-platform/metadata + - api-platform/hydra + - api-platform/json-api + - api-platform/json-schema + - api-platform/elasticsearch + - api-platform/openapi + - api-platform/graphql + - api-platform/http-cache + - api-platform/ramsey-uuid + - api-platform/serializer + - api-platform/state + - api-platform/symfony + - api-platform/validator + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php.version }} + tools: pecl, composer + extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite, mongodb + ini-values: memory_limit=-1 + - name: Linking + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . --permanent + - name: Run ${{ matrix.component }} install + run: | + composer ${{matrix.component}} update + - name: Run ${{ matrix.component }} tests + run: | + mkdir -p /tmp/build/logs/phpunit + cd $(composer ${{matrix.component}} --cwd) + ./vendor/bin/phpunit --fail-on-deprecation --display-deprecations --log-junit "/tmp/build/logs/phpunit/junit.xml" + behat: name: Behat (PHP ${{ matrix.php }}) runs-on: ubuntu-latest @@ -339,15 +389,13 @@ jobs: strategy: matrix: php: - - '8.1' - '8.2' - '8.3' + - '8.4' include: - - php: '8.1' - coverage: true - php: '8.2' - coverage: true - php: '8.3' + - php: '8.4' coverage: true fail-fast: false steps: @@ -371,9 +419,10 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests (PHP ${{ matrix.php }}) @@ -397,7 +446,7 @@ jobs: continue-on-error: true - name: Upload coverage results to Codecov if: matrix.coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: directory: build/logs/behat name: behat-php${{ matrix.php }} @@ -413,26 +462,6 @@ jobs: export PATH="$PATH:$HOME/.composer/vendor/bin" php-coveralls --coverage_clover=build/logs/behat/clover.xml continue-on-error: true - - name: Export OpenAPI documents - run: | - mkdir -p build/out/openapi - tests/Fixtures/app/console api:openapi:export -o build/out/openapi/openapi_v3.json - tests/Fixtures/app/console api:openapi:export --yaml -o build/out/openapi/openapi_v3.yaml - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: '14' - - name: Validate OpenAPI documents - run: | - npx swagger-cli validate build/out/openapi/openapi_v3.json - npx swagger-cli validate build/out/openapi/openapi_v3.yaml - - name: Upload OpenAPI artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: openapi-docs-php${{ matrix.php }} - path: build/out/openapi - continue-on-error: true postgresql: name: Behat (PHP ${{ matrix.php }}) (PostgreSQL) @@ -441,7 +470,7 @@ jobs: strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false env: APP_ENV: postgres @@ -474,9 +503,10 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests @@ -490,7 +520,7 @@ jobs: strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false services: mysql: @@ -524,9 +554,10 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests @@ -539,9 +570,7 @@ jobs: strategy: matrix: php: - - '8.1' - - '8.2' - - '8.3' + - '8.4' fail-fast: false env: APP_ENV: mongodb @@ -577,14 +606,16 @@ jobs: restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies run: | - composer update --no-interaction --no-progress --ansi + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . --permanent composer require --dev doctrine/mongodb-odm-bundle - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --group mongodb + run: vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --exclude-group=orm + - name: Clear test app cache + run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: | mkdir -p build/logs/behat @@ -604,7 +635,7 @@ jobs: path: build/logs/behat continue-on-error: true - name: Upload coverage results to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: directory: build/logs/behat name: behat-php${{ matrix.php }} @@ -627,9 +658,7 @@ jobs: strategy: matrix: php: - - '8.1' - - '8.2' - - '8.3' + - '8.4' fail-fast: false env: APP_ENV: mercure @@ -669,13 +698,14 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --group mercure + run: vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml --group mercure - name: Run Behat tests run: | mkdir -p build/logs/behat @@ -695,7 +725,7 @@ jobs: path: build/logs/behat continue-on-error: true - name: Upload coverage results to Codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: directory: build/logs/behat name: behat-php${{ matrix.php }} @@ -711,14 +741,14 @@ jobs: php-coveralls --coverage_clover=build/logs/behat/clover.xml continue-on-error: true - elasticsearch: - name: Behat (PHP ${{ matrix.php }}) (Elasticsearch) + elasticsearch-v9: + name: Behat (PHP ${{ matrix.php }}) (Elasticsearch v9) runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false env: APP_ENV: elasticsearch @@ -734,7 +764,7 @@ jobs: - name: Runs Elasticsearch uses: elastic/elastic-github-actions/elasticsearch@master with: - stack-version: '8.4.0' + stack-version: '9.0.0' security-enabled: false - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -754,22 +784,23 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: vendor/bin/behat --out=std --format=progress --profile=elasticsearch --no-interaction - elasticsearch-lowest: - name: Behat (PHP ${{ matrix.php }}) (Elasticsearch 7) (Symfony lowest) + elasticsearch-v8: + name: Behat (PHP ${{ matrix.php }}) (Elasticsearch v8) runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false env: APP_ENV: elasticsearch @@ -785,7 +816,8 @@ jobs: - name: Runs Elasticsearch uses: elastic/elastic-github-actions/elasticsearch@master with: - stack-version: '7.6.0' + stack-version: '8.4.0' + security-enabled: false - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -804,14 +836,18 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --prefer-lowest --no-interaction --no-progress --ansi + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . + composer require elasticsearch/elasticsearch "^8.4" -W - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: vendor/bin/behat --out=std --format=progress --profile=elasticsearch --no-interaction - phpunit-no-deprecations: - name: PHPUnit (PHP ${{ matrix.php }}) (no deprecations) + elasticsearch-v7: + name: Behat (PHP ${{ matrix.php }}) (Elasticsearch v7) runs-on: ubuntu-latest timeout-minutes: 20 strategy: @@ -819,9 +855,22 @@ jobs: php: - '8.3' fail-fast: false + env: + APP_ENV: elasticsearch steps: - name: Checkout uses: actions/checkout@v4 + - name: Configure sysctl limits + run: | + sudo swapoff -a + sudo sysctl -w vm.swappiness=1 + sudo sysctl -w fs.file-max=262144 + sudo sysctl -w vm.max_map_count=262144 + - name: Runs Elasticsearch + uses: elastic/elastic-github-actions/elasticsearch@master + with: + stack-version: '7.17.0' + security-enabled: false - name: Setup PHP uses: shivammathur/setup-php@v2 with: @@ -840,22 +889,24 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . + composer update elasticsearch/elasticsearch --prefer-lowest - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit + - name: Run Behat tests + run: vendor/bin/behat --out=std --format=progress --profile=elasticsearch --no-interaction - phpunit-symfony-next: - name: PHPUnit (PHP ${{ matrix.php }}) (Symfony dev) + phpunit-no-deprecations: + name: PHPUnit (PHP ${{ matrix.php }}) (no deprecations) runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false steps: - name: Checkout @@ -871,34 +922,30 @@ jobs: - name: Get composer cache directory id: composercache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - - name: Allow unstable project dependencies - run: composer config minimum-stability dev - name: Cache dependencies uses: actions/cache@v4 with: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - - name: Remove cache - run: rm -Rf tests/Fixtures/app/var/cache/* - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - # The PHPUnit Bridge doesn't support PHPUnit 10 yet: https://github.com/symfony/symfony/issues/49069 - run: vendor/bin/phpunit -c phpunit10.xml.dist + run: vendor/bin/phpunit --fail-on-deprecation --display-deprecations - behat-symfony-next: - name: Behat (PHP ${{ matrix.php }}) (Symfony dev) + phpunit-symfony-next: + name: PHPUnit (PHP ${{ matrix.php }}) (Symfony dev) runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false steps: - name: Checkout @@ -911,8 +958,6 @@ jobs: extensions: intl, bcmath, curl, openssl, mbstring, mongodb coverage: none ini-values: memory_limit=-1 - - name: Install additional packages - run: sudo apt-get install moreutils - name: Get composer cache directory id: composercache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT @@ -927,65 +972,59 @@ jobs: - name: Remove cache run: rm -Rf tests/Fixtures/app/var/cache/* - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - - name: Run Behat tests - run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction + - name: Run PHPUnit tests + run: vendor/bin/phpunit --fail-on-deprecation - windows-phpunit: - name: Windows PHPUnit (PHP ${{ matrix.php }}) (SQLite) - runs-on: windows-latest + behat-symfony-next: + name: Behat (PHP ${{ matrix.php }}) (Symfony dev) + runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false - env: - SYMFONY_PHPUNIT_VERSION: '9.5' - APP_ENV: sqlite - DATABASE_URL: sqlite:///%kernel.project_dir%/var/data.db steps: - name: Checkout uses: actions/checkout@v4 - - name: Setup PHP with pre-release PECL extension + - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} tools: pecl, composer - extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite + extensions: intl, bcmath, curl, openssl, mbstring, mongodb coverage: none ini-values: memory_limit=-1 - # Not in pecl - - name: Setup mongodb - run: | - curl -sLO https://github.com/mongodb/mongo-php-driver/releases/download/1.17.2/php_mongodb-1.17.2-8.3-nts-x64.zip - unzip -q php_mongodb-1.17.2-8.3-nts-x64.zip php_mongodb.dll - mv php_mongodb.dll C:\tools\php\ext - echo "extension=php_mongodb.dll" >> C:\tools\php\php.ini + - name: Install additional packages + run: sudo apt update && sudo apt-get install moreutils - name: Get composer cache directory id: composercache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - shell: bash + - name: Allow unstable project dependencies + run: composer config minimum-stability dev - name: Cache dependencies uses: actions/cache@v4 with: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- + - name: Remove cache + run: rm -Rf tests/Fixtures/app/var/cache/* - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install phpunit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml - env: - SYMFONY_DEPRECATIONS_HELPER: max[direct]=0&ignoreFile=./tests/.ignored-deprecations + - name: Run Behat tests + run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction windows-behat: name: Windows Behat (PHP ${{ matrix.php }}) (SQLite) @@ -994,10 +1033,9 @@ jobs: strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false env: - SYMFONY_PHPUNIT_VERSION: '9.5' APP_ENV: sqlite DATABASE_URL: sqlite:///%kernel.project_dir%/var/data.db steps: @@ -1008,33 +1046,37 @@ jobs: with: php-version: ${{ matrix.php }} tools: pecl, composer - extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite + extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite, fileinfo, mongodb coverage: none ini-values: memory_limit=-1 - # Not in pecl - - name: Setup mongodb - run: | - curl -sLO https://github.com/mongodb/mongo-php-driver/releases/download/1.17.2/php_mongodb-1.17.2-8.3-nts-x64.zip - unzip -q php_mongodb-1.17.2-8.3-nts-x64.zip php_mongodb.dll - mv php_mongodb.dll C:\tools\php\ext - echo "extension=php_mongodb.dll" >> C:\tools\php\php.ini - name: Get composer cache directory id: composercache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT shell: bash + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies uses: actions/cache@v4 with: path: ${{ steps.composercache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- + - name: Keep windows path + id: get-cwd + shell: bash + run: | + cwd=$(php -r 'echo(str_replace("\\", "\\\\", $_SERVER["argv"][1]));' '${{ github.workspace }}') + echo cwd=$cwd >> $GITHUB_OUTPUT - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install phpunit - run: vendor/bin/simple-phpunit --version + shell: bash + run: | + php -m + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . --working-directory='${{ steps.get-cwd.outputs.cwd }}' - name: Clear test app cache + shell: bash run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests + shell: bash run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction phpunit-symfony-lowest: @@ -1069,13 +1111,15 @@ jobs: - name: Remove cache run: rm -Rf tests/Fixtures/app/var/cache/* - name: Update project dependencies - run: composer update --prefer-lowest --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . --permanent + composer update --prefer-lowest - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run PHPUnit tests - run: vendor/bin/simple-phpunit + run: vendor/bin/phpunit env: SYMFONY_DEPRECATIONS_HELPER: max[self]=0&ignoreFile=./tests/.ignored-deprecations @@ -1086,7 +1130,7 @@ jobs: strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false steps: - name: Checkout @@ -1100,7 +1144,7 @@ jobs: coverage: none ini-values: memory_limit=-1 - name: Install additional packages - run: sudo apt-get install moreutils + run: sudo apt update && sudo apt-get install moreutils - name: Get composer cache directory id: composercache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT @@ -1113,24 +1157,28 @@ jobs: - name: Remove cache run: rm -Rf tests/Fixtures/app/var/cache/* - name: Update project dependencies - run: composer update --prefer-lowest --no-interaction --no-progress --ansi + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . --permanent + composer update --prefer-lowest - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests run: vendor/bin/behat --out=std --format=progress --profile=default --no-interaction --tags='~@disableForSymfonyLowest' - phpunit_legacy: - name: PHPUnit Legacy event listeners (PHP ${{ matrix.php }}) + phpunit_listeners: + name: PHPUnit event listeners (PHP ${{ matrix.php }}) env: - EVENT_LISTENERS_BACKWARD_COMPATIBILITY_LAYER: 1 + USE_SYMFONY_LISTENERS: 1 runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: php: - - '8.3' + - '8.4' include: - - php: '8.3' + - php: '8.4' coverage: true fail-fast: false steps: @@ -1157,20 +1205,19 @@ jobs: if: matrix.coverage run: echo "COVERAGE=1" >> $GITHUB_ENV - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - - name: Use legacy ignored deprecations - run: cp tests/.ignored-deprecations-legacy-events tests/.ignored-deprecations - name: Run PHPUnit tests run: | mkdir -p build/logs/phpunit if [ "$COVERAGE" = '1' ]; then - vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml + vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml --coverage-clover build/logs/phpunit/clover.xml else - vendor/bin/simple-phpunit --log-junit build/logs/phpunit/junit.xml + vendor/bin/phpunit --log-junit build/logs/phpunit/junit.xml fi - name: Upload test artifacts if: always() @@ -1181,7 +1228,7 @@ jobs: continue-on-error: true - name: Upload coverage results to Codecov if: matrix.coverage - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: directory: build/logs/phpunit name: phpunit-php${{ matrix.php }} @@ -1198,16 +1245,16 @@ jobs: php-coveralls --coverage_clover=build/logs/phpunit/clover.xml continue-on-error: true - behat_legacy: - name: Behat Legacy event listeners (PHP ${{ matrix.php }}) + behat_listeners: + name: Behat event listeners (PHP ${{ matrix.php }}) env: - EVENT_LISTENERS_BACKWARD_COMPATIBILITY_LAYER: 1 + USE_SYMFONY_LISTENERS: 1 runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false steps: - name: Checkout @@ -1230,15 +1277,16 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - name: Run Behat tests (PHP 8) run: | mkdir -p build/logs/behat - vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=legacy --no-interaction + vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=symfony_listeners --no-interaction - name: Upload test artifacts if: always() uses: actions/upload-artifact@v4 @@ -1246,37 +1294,15 @@ jobs: name: behat-logs-php${{ matrix.php }} path: build/logs/behat continue-on-error: true - - name: Export OpenAPI documents - run: | - mkdir -p build/out/openapi - tests/Fixtures/app/console api:openapi:export -o build/out/openapi/openapi_v3.json - tests/Fixtures/app/console api:openapi:export --yaml -o build/out/openapi/openapi_v3.yaml - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: '14' - - name: Validate OpenAPI documents - run: | - npx swagger-cli validate build/out/openapi/openapi_v3.json - npx swagger-cli validate build/out/openapi/openapi_v3.yaml - - name: Upload OpenAPI artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: openapi-docs-php${{ matrix.php }} - path: build/out/openapi - continue-on-error: true - behat_listeners: - name: Behat event listeners (PHP ${{ matrix.php }}) - env: - USE_SYMFONY_LISTENERS: 1 + openapi: + name: OpenAPI runs-on: ubuntu-latest timeout-minutes: 20 strategy: matrix: php: - - '8.3' + - '8.4' fail-fast: false steps: - name: Checkout @@ -1286,9 +1312,12 @@ jobs: with: php-version: ${{ matrix.php }} tools: pecl, composer - extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite - coverage: pcov + extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite, mongodb ini-values: memory_limit=-1 + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: '22' - name: Get composer cache directory id: composercache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT @@ -1299,39 +1328,87 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi - - name: Install PHPUnit - run: vendor/bin/simple-phpunit --version + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . - name: Clear test app cache run: tests/Fixtures/app/console cache:clear --ansi - - name: Run Behat tests (PHP 8) - run: | - mkdir -p build/logs/behat - vendor/bin/behat --out=std --format=progress --format=junit --out=build/logs/behat/junit --profile=symfony_listeners --no-interaction - - name: Upload test artifacts - if: always() - uses: actions/upload-artifact@v4 - with: - name: behat-logs-php${{ matrix.php }} - path: build/logs/behat - continue-on-error: true - name: Export OpenAPI documents run: | mkdir -p build/out/openapi - tests/Fixtures/app/console api:openapi:export -o build/out/openapi/openapi_v3.json tests/Fixtures/app/console api:openapi:export --yaml -o build/out/openapi/openapi_v3.yaml - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: '14' - name: Validate OpenAPI documents run: | - npx swagger-cli validate build/out/openapi/openapi_v3.json - npx swagger-cli validate build/out/openapi/openapi_v3.yaml - - name: Upload OpenAPI artifacts - if: always() - uses: actions/upload-artifact@v4 + npm i -g @quobix/vacuum + vacuum lint -r tests/Fixtures/app/ruleset.yaml build/out/openapi/openapi_v3.yaml -d --ignore-array-circle-ref --ignore-polymorph-circle-ref -b --no-clip + + laravel: + name: Laravel (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + matrix: + php: + - '8.2' + - '8.3' + - '8.4' + include: + - php: '8.2' + - php: '8.3' + - php: '8.4' + coverage: true + fail-fast: false + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 with: - name: openapi-docs-php${{ matrix.php }} - path: build/out/openapi - continue-on-error: true + php-version: ${{ matrix.php }} + tools: pecl, composer + extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite, mongodb + ini-values: memory_limit=-1 + - name: Update project dependencies + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . --permanent + composer api-platform/laravel update + - name: PHP version tweaks + run: | + composer run-script build + composer run-script test + working-directory: 'src/Laravel' + + laravel-e2e: + name: Laravel E2E installation (PHP 8.4) + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.4 + tools: pecl, composer + extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite, mongodb + ini-values: memory_limit=-1 + - name: Update project dependencies + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link . --permanent + - name: Setup laravel project + run: | + composer global require laravel/installer + laravel new test-api-platform-install --pest --no-interaction + - name: Install api-platform/laravel + run: | + composer global link ../src/Laravel --permanent + composer config minimum-stability dev + composer config prefer-stable true + composer require api-platform/laravel:"@dev" + php ./artisan api-platform:install + working-directory: 'test-api-platform-install' diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 00000000000..5ac59ff7c40 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,32 @@ +name: Commit Lint + +on: + pull_request_target: + types: [opened, reopened, synchronize] + +jobs: + commitlint: + if: github.event_name == 'pull_request_target' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch PR head + run: git fetch origin pull/${{ github.event.pull_request.number }}/head:pr_head + - name: Run commitlint + run: | + merge_base_sha=$(git merge-base HEAD pr_head) + first_commit_sha=$(git rev-list --no-merges --reverse $merge_base_sha..pr_head | head -n 1) + + if [ -z "$first_commit_sha" ]; then + echo "Could not determine the first commit of the PR. Skipping." + exit 0 + fi + + commit_message=$(git log -1 --pretty=%B $first_commit_sha) + # we can't use npx see https://github.com/conventional-changelog/commitlint/issues/613 + echo '{}' > package.json + npm install --no-fund --no-audit @commitlint/config-conventional @commitlint/cli + echo "$commit_message" | ./node_modules/.bin/commitlint -g .commitlintrc + diff --git a/.github/workflows/guides.yaml b/.github/workflows/guides.yaml deleted file mode 100644 index b4613285c47..00000000000 --- a/.github/workflows/guides.yaml +++ /dev/null @@ -1,63 +0,0 @@ -name: Guides - -on: - push: - pull_request: - -env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERAGE: '0' - SYMFONY_DEPRECATIONS_HELPER: max[self]=0 - -jobs: - docs: - name: Test guides - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Setup PHP with pre-release PECL extension - uses: shivammathur/setup-php@v2 - with: - php-version: 8.2 - tools: pecl, composer - extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite - coverage: none - ini-values: memory_limit=-1 - - name: Get composer cache directory - id: composercache - run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - shell: bash - - name: Global require pdg - run: | - cd $(composer -n config --global home) - echo "{\"repositories\":[{\"type\":\"vcs\",\"url\":\"https://github.com/php-documentation-generator/php-documentation-generator/"}]}" > composer.json - composer global config --no-plugins allow-plugins.symfony/runtime true - composer global require php-documentation-generator/php-documentation-generator:dev-main - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: ${{ steps.composercache.outputs.dir }} - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ runner.os }}-composer- - - name: Install project dependencies - working-directory: docs - run: | - composer update --no-interaction --no-progress --ansi - cp -r ../src ./vendor/api-platform/core/ - - name: Test guides - working-directory: docs - env: - APP_DEBUG: 0 - PDG_AUTOLOAD: ${{ github.workspace }}/docs/vendor/autoload.php - KERNEL_CLASS: \ApiPlatform\Playground\Kernel - run: | - for d in guides/*.php; do - rm -f var/*.db - echo "Testing guide $d" - pdg-phpunit $d || exit 1 - exit_status=$? - if [ $exit_status -ne 0 ]; then - exit $exit_status - fi - done diff --git a/.github/workflows/guides.yml b/.github/workflows/guides.yml new file mode 100644 index 00000000000..388f1c0c39f --- /dev/null +++ b/.github/workflows/guides.yml @@ -0,0 +1,68 @@ +name: Guides + +on: + push: + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERAGE: '0' + SYMFONY_DEPRECATIONS_HELPER: max[self]=0 + +jobs: + docs: + name: Test guides + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup PHP with pre-release PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + tools: pecl, composer + extensions: intl, bcmath, curl, openssl, mbstring, pdo_sqlite + coverage: none + ini-values: memory_limit=-1 + - name: Get composer cache directory + id: composercache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + shell: bash + - name: Global require pdg + run: | + cd $(composer -n config --global home) + echo "{\"repositories\":[{\"type\":\"vcs\",\"url\":\"https://github.com/php-documentation-generator/php-documentation-generator/"}]}" > composer.json + composer global config --no-plugins allow-plugins.symfony/runtime true + composer global require php-documentation-generator/php-documentation-generator:dev-main + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + - name: Install project dependencies + working-directory: docs + run: | + composer global require soyuka/pmu + composer global config allow-plugins.soyuka/pmu true --no-interaction + composer global link .. + - name: Test guides + working-directory: docs + env: + APP_DEBUG: 0 + PDG_AUTOLOAD: ${{ github.workspace }}/docs/vendor/autoload.php + KERNEL_CLASS: \ApiPlatform\Playground\Kernel + run: | + for d in guides/*.php; do + rm -f var/*.db + echo "Testing guide $d" + pdg-phpunit $d || exit 1 + exit_status=$? + if [ $exit_status -ne 0 ]; then + exit $exit_status + fi + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..56699219aec --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: Release Pipeline + +on: + push: + tags: + - 'v*' + branches: + - main + - '[0-9].*' + - '[0-9][0-9].*' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + split: + name: Subtree Split + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Generate App Token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.API_PLATFORM_APP_ID }} + private_key: ${{ secrets.API_PLATFORM_APP_PRIVATE_KEY }} + + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ steps.generate_token.outputs.token }} + fetch-depth: 0 + + - name: Install splitsh + run: | + curl -L https://github.com/splitsh/lite/releases/download/v1.0.1/lite_linux_amd64.tar.gz > lite_linux_amd64.tar.gz + tar -zxpf lite_linux_amd64.tar.gz + chmod +x splitsh-lite + echo "$(pwd)" >> $GITHUB_PATH + + - name: Split to manyrepo + run: find src -maxdepth 3 -name composer.json -print0 | xargs -I '{}' -n 1 -0 bash subtree.sh {} ${{ github.ref }} + + dispatch-distribution-update: + name: Dispatch Distribution Update + runs-on: ubuntu-latest + needs: split + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Generate App Token + id: generate_token + uses: tibdex/github-app-token@v2 + with: + app_id: ${{ secrets.API_PLATFORM_APP_ID }} + private_key: ${{ secrets.API_PLATFORM_APP_PRIVATE_KEY }} + + - name: Update distribution + env: + GH_TOKEN: ${{ steps.generate_token.outputs.token }} + run: gh workflow run -R api-platform/api-platform release.yml -f tag=${{ github.ref_name }} diff --git a/.github/workflows/subtree.yml b/.github/workflows/subtree.yml deleted file mode 100644 index 394f1c7cb1e..00000000000 --- a/.github/workflows/subtree.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: subtree -on: - push: - tags: - - v* - branches: - - main - - 3.1 - - 3.2 - -env: - COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -jobs: - split: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - token: ${{ secrets.GH_TOKEN }} - - name: 'Install splitsh' - run: | - curl -L https://github.com/splitsh/lite/releases/download/v1.0.1/lite_linux_amd64.tar.gz > lite_linux_amd64.tar.gz - tar -zxpf lite_linux_amd64.tar.gz - chmod +x splitsh-lite - echo "$(pwd)" >> $GITHUB_PATH - - name: 'Split to manyrepo' - run: find src -maxdepth 3 -name composer.json -print0 | xargs -I '{}' -n 1 -0 bash subtree.sh {} ${{ github.ref }} diff --git a/.gitignore b/.gitignore index 57d0a18c303..b44218aedc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea *.log /.php-cs-fixer.php /.php-cs-fixer.cache diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index f790b3b5e99..365dd5b6ff1 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -15,6 +15,8 @@ ->in(__DIR__) ->exclude([ 'src/Core/Bridge/Symfony/Maker/Resources/skeleton', + 'src/Laravel/Console/Maker/Resources/skeleton', + 'src/Laravel/config', 'tests/Fixtures/app/var', 'docs/guides', 'docs/var', @@ -22,14 +24,6 @@ 'src/Doctrine/Odm/Tests/var' ]) ->notPath('src/Symfony/Bundle/DependencyInjection/Configuration.php') - ->notPath('src/Annotation/ApiFilter.php') // temporary - ->notPath('src/Annotation/ApiProperty.php') // temporary - ->notPath('src/Annotation/ApiResource.php') // temporary - ->notPath('src/Annotation/ApiSubresource.php') // temporary - ->notPath('tests/Fixtures/TestBundle/Entity/DummyPhp8.php') // temporary - ->notPath('tests/Fixtures/TestBundle/Enum/EnumWithDescriptions.php') // PHPDoc on enum cases - ->notPath('tests/Fixtures/TestBundle/Enum/GamePlayMode.php') // PHPDoc on enum cases - ->notPath('tests/Fixtures/TestBundle/Enum/GenderTypeEnum.php') // PHPDoc on enum cases ->append([ 'tests/Fixtures/app/console', ]); @@ -39,7 +33,7 @@ ->setRules([ '@DoctrineAnnotation' => true, '@PHP71Migration' => true, - '@PHP71Migration:risky' => true, + '@PHP70Migration:risky' => true, '@PHPUnit60Migration:risky' => true, '@Symfony' => true, '@Symfony:risky' => true, @@ -82,7 +76,7 @@ ], 'no_superfluous_elseif' => true, 'no_superfluous_phpdoc_tags' => [ - 'allow_mixed' => false, + 'allow_mixed' => true, // For PHPStan 'allow_unused_params' => true, ], 'no_unset_cast' => true, @@ -104,9 +98,6 @@ 'php_unit_test_annotation' => [ 'style' => 'prefix', ], - 'phpdoc_add_missing_param_annotation' => [ - 'only_untyped' => true, - ], 'phpdoc_no_alias_tag' => true, 'phpdoc_order' => true, 'phpdoc_trim_consecutive_blank_line_separation' => true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 790603e8c06..b4ad8a2945d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,1279 @@ # Changelog +## v4.2.2 + +### Bug fixes + +* [0b8237918](https://github.com/api-platform/core/commit/0b8237918774e4765f35f13521a18d4c36ac0ea7) fix(state): object mapper with input different (#7435) +* [1094a52d6](https://github.com/api-platform/core/commit/1094a52d68ef8d0982c09f978a0d5c1b2da6734b) fix(metadata): allow description and other fields to be override seperately (#7442) +* [133028e38](https://github.com/api-platform/core/commit/133028e38a2135725bdc7463c7fe2d013ca81afb) fix(state): object mapper on delete operation (#7447) +* [1a4636200](https://github.com/api-platform/core/commit/1a46362004ecfdf6891b0e3daa1044cf80d25605) fix(doctrine): group or filter in an AndWhere #7441 (#7445) +* [55fd65795](https://github.com/api-platform/core/commit/55fd65795b6b2ea6def8ef2cc6dc0cc107e220dd) fix(validator): validation exception without constraint violation list +* [5b59f8914](https://github.com/api-platform/core/commit/5b59f8914f40889f35b7017102049cfd537c22ab) fix(jsonschema/jsonld): make `@id` and `@type` properties required only in the JSON-LD schema for output (#7397) +* [654339e03](https://github.com/api-platform/core/commit/654339e03d6eff2b7486dcd35d145e963f2c5d83) fix(openapi): Improve response override (#7428) +* [6c267408a](https://github.com/api-platform/core/commit/6c267408a099aceeef44a2410361963024153adc) fix(hydra): genId false schema (#7440) +* [70bdf2959](https://github.com/api-platform/core/commit/70bdf2959c29ceca091f00b362a1872b0c2a27bc) fix(symfony): ensure the kernel is booted before using `KernelBrowser::loginUser()` (#7446) +* [9c918c657](https://github.com/api-platform/core/commit/9c918c65795fcb8b27ea82f2fb14f8dcd79b09ef) fix(symfony): align listeners context (#7449) +* [abe0438be](https://github.com/api-platform/core/commit/abe0438be3abf82896ae7937a9a78795361d7179) fix(jsonschema): make all required properties optional in PATCH operation with 'json' format (#7398) +* [e1ce9456b](https://github.com/api-platform/core/commit/e1ce9456b20067df22dea968f5089046110ecf6c) fix(openapi): define items type for HydraCollectionBaseSchema hydra:member (#7419) +* [e52e825db](https://github.com/api-platform/core/commit/e52e825dbd4af1ff1a9a1dca49ded68481dcec10) fix(openapi): allow assertMatchesJsonSchema with custom output dto (#7438) +* [f3c811d0f](https://github.com/api-platform/core/commit/f3c811d0f4b7bb6ac7dcf0967bdbeba65069a362) fix(hydra): add base schema to item of a collection (#7444) + +## v4.2.1 + +### Bug fixes + +* [059e6a09e](https://github.com/api-platform/core/commit/059e6a09e272f7422390df83f5488ed81ea8d5b3) fix(symfony): remove suggest ocramius/package-versions (#7395) +* [3b0c44177](https://github.com/api-platform/core/commit/3b0c4417743187a3f6e1e61c92370dd11a71734d) fix(openapi): ability to override description in response (#7412) +* [54bdce504](https://github.com/api-platform/core/commit/54bdce5042b6b712a7e32e54bfcda7af4833f669) fix(symfony): openapi property path for validation +* [65e137fca](https://github.com/api-platform/core/commit/65e137fca534cae02dd746c87ce99e32d8cace3b) fix: make default value of `pagination_maximum_items_per_page` same as Laravel version (#7396) +* [6916bea95](https://github.com/api-platform/core/commit/6916bea95effc959967ef56618f913323d340a18) fix(serializer): require api-platform/metadata:^4.2 (#7409) +* [82d0dd7e0](https://github.com/api-platform/core/commit/82d0dd7e0dca66be608f102717b2868ef3696f94) fix(symfony): openapi property path for validation (#7411) +* [a5102d9a2](https://github.com/api-platform/core/commit/a5102d9a2b7cf36313234635a24ba235f2ac912d) fix(schema): anyOf must contains an array, not an object (#7399) +* [dfe4fd7f2](https://github.com/api-platform/core/commit/dfe4fd7f24aa2419e9cd0fe9478d10b88ffe6569) fix: handle generic array in JsonSchema (#7414) + +Also includes [v4.1.25 bug fixes](#v4125) + +### Features + +## v4.2.0 + +### Features + +* [ceeaf51e8](https://github.com/api-platform/core/commit/ceeaf51e806e772c2801cfaaa1286fc081a10a3e) feat: json streamer (#7225) +* [092a3a4f8](https://github.com/api-platform/core/commit/092a3a4f89563109b3dac18351da25051f7aa745) feat(mongodb): partial paginator (#7352) +* [1290ebb88](https://github.com/api-platform/core/commit/1290ebb88680dde94e536dfc19d9b330612e0909) feat(serializer): ability to throw access denied exception when denormalizing secured properties (#7221) +* [1862d03b7](https://github.com/api-platform/core/commit/1862d03b77c941cac13ed7bcbf1d555d50614b4f) feat(symfony): add error classes options to open api config (#7143) +* [24a1cf5a2](https://github.com/api-platform/core/commit/24a1cf5a2f9d56df791abdb625ec83594ee4f0f9) feat(elasticsearch): add support for v9 (#7180) +* [26d2394b7](https://github.com/api-platform/core/commit/26d2394b70342f99bb8a4699529f7a5c113830bb) feat(doctrine): new search filters (#7121) +* [2a13100f4](https://github.com/api-platform/core/commit/2a13100f4edeb02191a3da891d99c1d054710166) feat(serializer): (un)set object-to-populate through denormalization context (#7124) +* [3ab0b8d0f](https://github.com/api-platform/core/commit/3ab0b8d0fba6a56014752d90a007f57483502551) feat(metadata): use PHP file as resource format +* [4abf17b3c](https://github.com/api-platform/core/commit/4abf17b3c49491ee1f77f951028422dacae90456) feat(symfony): CSS Color Schema Restriction for Property Validation (#7215) +* [4b1b94cad](https://github.com/api-platform/core/commit/4b1b94cad1bdd2e9c3d94cc22b7e3519a0d6c609) feat(symfony): Autoconfigure classes using `#[ApiResource]` attribute (#6943) +* [6491bfc7a](https://github.com/api-platform/core/commit/6491bfc7a88af5722a98645ff648a461c9990b7e) feat(doctrine): improve http cache invalidation using the info from the mapping (#7319) +* [9e9cf648d](https://github.com/api-platform/core/commit/9e9cf648d6cbe21f9c7b07d54e797ac08ffc45eb) feat(metadata): introduce metadata mutators for resource & operations (#7213) +* [a42034dc3](https://github.com/api-platform/core/commit/a42034dc384ab1b372ea5bf4452db37791c3b739) feat(symfony): object mapper with state options (#6801) +* [a8b82172a](https://github.com/api-platform/core/commit/a8b82172afa48f04438786aa13d00a33eb8956e1) feat(doctrine): support integer-backed enums in BackedEnumFilter (#7129) +* [b52187d91](https://github.com/api-platform/core/commit/b52187d91eca6cf08d60dee80d19d42516a7e8b7) feat(serializer): handle defaultType for DiscriminatorMap (#7284) +* [b948209b9](https://github.com/api-platform/core/commit/b948209b97a3a952b884c8c755c1bb9e63e81689) feat(laravel): add make:filter command to generate API Platform filters (#7364) +* [ebd85f502](https://github.com/api-platform/core/commit/ebd85f50218988443662f37e2eb7f5d85026f6cf) feat(symfony): add make:filter command to generate API Platform filters (#7355) +* [bf09616cf](https://github.com/api-platform/core/commit/bf09616cfbbc2c19a9d04a7be3edc8a9bc0c07ef) feat(httpcache): add more cache directives to AddHeadersProcessor (#7008) +* [f25d7d1a6](https://github.com/api-platform/core/commit/f25d7d1a6e0ed3fb60a01ccda60729721a82d215) feat(metadata): ability to set description on an Error (#7329) + +### Breaking changes + +* [073c0e282](https://github.com/api-platform/core/commit/073c0e2822c9d93d3303ee64178bed19e6e61080) fix(openapi)!: allowReserved, allowEmtpyValue defaults to null +* [62a9cc821](https://github.com/api-platform/core/commit/62a9cc821dc0d6469e434b5d18339af75b15fc6f) fix(state)!: parameter default value overrides falsy value +* [79edced67](https://github.com/api-platform/core/commit/79edced67ccca1a7b80455dd94203501d9c4fa89) fix(json-schema): share invariable sub-schemas + +### TypeInfo + +* [143d512ef](https://github.com/api-platform/core/commit/143d512ef24b988a9b80b3da28c9a36e4b346c76) feat(jsonapi): use `TypeInfo`'s `Type` (#7100) +* [1d579d095](https://github.com/api-platform/core/commit/1d579d095616a6025d63526f14402eaef173346b) feat(hal): use `TypeInfo` type (#7097) +* [350d6d707](https://github.com/api-platform/core/commit/350d6d707a95587679b13c2a8670206b480d9ab2) feat(hydra): use `TypeInfo`'s `Type` (#7099) +* [55461a83c](https://github.com/api-platform/core/commit/55461a83cbb804fb73167d5196337b6a04e38c22) feat: Use `Type` of `TypeInfo` instead of `PropertyInfo` (#6979) +* [7c796de0d](https://github.com/api-platform/core/commit/7c796de0d8d0dc6dd7c4d7a3da0afb61823b6258) feat(serializer): type info (#7104) +* [827e1f725](https://github.com/api-platform/core/commit/827e1f725b5f2a560cd0aaf9b63e7e7e8a9b329d) feat(elasticsearch): use `TypeInfo` type (#7098) +* [a1952e304](https://github.com/api-platform/core/commit/a1952e30403c2ccf98d5cffb667720c3f4c753bf) feat(openapi): use `TypeInfo`'s `Type` (#7096) +* [dd3356824](https://github.com/api-platform/core/commit/dd33568245de962ff99efcd8079d2c6f8daf8061) feat(laravel): use `TypeInfo`'s `Type` (#7101) + +### Bug fixes + +* [02a764950](https://github.com/api-platform/core/commit/02a7649509f3f69abb74c94805707b0c253521fb) fix(symfony): explicitly set the target class when mapping entities to resources (#7311) +* [0411cd9cc](https://github.com/api-platform/core/commit/0411cd9cc73f085a1918a13a3798e0c5aee5bbff) fix(jsonld): duplicate error fields when prefix is enabled (#7021) (#7074) +* [1acf8f764](https://github.com/api-platform/core/commit/1acf8f764f5a3fca56ed2f970d61f737226c694a) fix(symfony): only set name_converter for the default serializer (#6101) (#7365) +* [2c5c9e451](https://github.com/api-platform/core/commit/2c5c9e4516944ccfe11e1e69cfaba87f1cd85da4) fix(openapi): no content schema (#7384) +* [321c68f12](https://github.com/api-platform/core/commit/321c68f1216c9ea1486e0d6b08a228d50fdf17a6) fix: pagination via cursor on ApiResource operations (#7368) +* [5a8d4d282](https://github.com/api-platform/core/commit/5a8d4d2827fba63868cc6b392c383fd8ae5c65cd) fix(jsonld): various json streamer fixes (#7374) +* [9389b4f46](https://github.com/api-platform/core/commit/9389b4f4637b925e55ee68c3f62c88828014c14f) fix(laravel): restore accidentally removed BooleanFilter (#6881) +* [b80ab9a59](https://github.com/api-platform/core/commit/b80ab9a5940e1638600e8281cf506ab9649912d3) fix(httpcache): collection iri invalidation for mapped entities (#7353) +* [c9692b509](https://github.com/api-platform/core/commit/c9692b509d5b641104addbadb349b9bcab83e251) fix(state): transform uri variable using ReadLinkParameterProvider (#7375) +* [d06b1a0a0](https://github.com/api-platform/core/commit/d06b1a0a0e9dcde7fcdb585b435cd617620b098b) fix(state): object-mapper reuse related entity (#7300) +* [ecb3f6cef](https://github.com/api-platform/core/commit/ecb3f6cefcf87a0e040cb528a6cb2b31ca97663a) fix(httpcache): only map entites that are persisted +* [d921dd37a](https://github.com/api-platform/core/commit/d921dd37a489646ae6f5760e1a853031ceaa8598) fix(mongodb): make `ParameterExtension` context more generic (#7389) + +### Miscellaneous + +* [04f252e7f](https://github.com/api-platform/core/commit/04f252e7ff7bf8705399dea17b1374027c31f6ff) fix(symfony): missing finder dependency +* [1806cfae6](https://github.com/api-platform/core/commit/1806cfae683f907b6800852e6b2c7bc9ffb59fc1) feat(symfony): stop watch system provider/processor +* [202c60fcb](https://github.com/api-platform/core/commit/202c60fcb05be41bbe0bf03a3314ab3b7e9fed64) feat(openapi): license identifier (#7141) +* [4f0864c1a](https://github.com/api-platform/core/commit/4f0864c1a1b9ecdfe0ba4488b99b84cf851240d3) fix(symfony): remove deprecation about jsonopenapi +* [6db55be8c](https://github.com/api-platform/core/commit/6db55be8c9e82717a49e92269ca96a2dbd41bf1e) fix(metadata): wrong method name in resource mutator +* [76c80a67c](https://github.com/api-platform/core/commit/76c80a67cdaf9615c598bf0fa3e37a89ce7589ba) fix: command name deprecation +* [77b292bfe](https://github.com/api-platform/core/commit/77b292bfefcf655b7ade95bd23046dff5c38c464) feat(metadata): class is now class-string (#7307) +* [95451fbee](https://github.com/api-platform/core/commit/95451fbee6a0d1cd984216eba0074916243ad701) feat(state): remove @internal from the CreateProvider +* [ce4e97fe8](https://github.com/api-platform/core/commit/ce4e97fe8c5e5f99f603a9efb372bdae1f855e4a) fix(hal): rename package +* [cff61eab8](https://github.com/api-platform/core/commit/cff61eab8643f8ed08d59c0684e77740d0d81b04) fix(metadata): append php file resource extractor (#7193) +* [de9e7f576](https://github.com/api-platform/core/commit/de9e7f576039d9943f7e9b70e41886d3a9406655) feat(hal): allow to output null links for hal+json +* [f010fd456](https://github.com/api-platform/core/commit/f010fd456dcb3b068b1033882d1cdc235d892d81) fix(serializer): deprecate SerializerAwareProviderInterface and SerializableProvider (#7348) +* [f3d4afe03](https://github.com/api-platform/core/commit/f3d4afe032385f3b665131a365e42706930f0730) fix(symfony): validator type-info + +## v4.2.0-beta.1 + +### Bug fixes + +* [2c5c9e451](https://github.com/api-platform/core/commit/2c5c9e4516944ccfe11e1e69cfaba87f1cd85da4) fix(openapi): no content schema (#7384) +* [4f0864c1a](https://github.com/api-platform/core/commit/4f0864c1a1b9ecdfe0ba4488b99b84cf851240d3) fix(symfony): remove deprecation about jsonopenapi +* [5a8d4d282](https://github.com/api-platform/core/commit/5a8d4d2827fba63868cc6b392c383fd8ae5c65cd) fix(jsonld): various json streamer fixes (#7374) +* [62a9cc821](https://github.com/api-platform/core/commit/62a9cc821dc0d6469e434b5d18339af75b15fc6f) fix(state)!: parameter default value overrides falsy value +* [6db55be8c](https://github.com/api-platform/core/commit/6db55be8c9e82717a49e92269ca96a2dbd41bf1e) fix(metadata): wrong method name in resource mutator +* [b80ab9a59](https://github.com/api-platform/core/commit/b80ab9a5940e1638600e8281cf506ab9649912d3) fix(httpcache): collection iri invalidation for mapped entities (#7353) +* [c9692b509](https://github.com/api-platform/core/commit/c9692b509d5b641104addbadb349b9bcab83e251) fix(state): transform uri variable using ReadLinkParameterProvider (#7375) +* [ce4e97fe8](https://github.com/api-platform/core/commit/ce4e97fe8c5e5f99f603a9efb372bdae1f855e4a) fix(hal): rename package + +### Features + +* [b948209b9](https://github.com/api-platform/core/commit/b948209b97a3a952b884c8c755c1bb9e63e81689) feat(laravel): add make:filter command to generate API Platform filters (#7364) +* [ebd85f502](https://github.com/api-platform/core/commit/ebd85f50218988443662f37e2eb7f5d85026f6cf) feat(symfony): add make:filter command to generate API Platform filters (#7355) + +Also includes patches from v4.1.24 and v4.2.0-alpha fixes + +## v4.2.0-alpha.3 + +### Bug fixes + +* [f010fd456](https://github.com/api-platform/core/commit/f010fd456dcb3b068b1033882d1cdc235d892d81) fix(serializer): deprecate SerializerAwareProviderInterface and SerializableProvider (#7348) +* [02a764950](https://github.com/api-platform/core/commit/02a7649509f3f69abb74c94805707b0c253521fb) fix(symfony): explicitly set the target class when mapping entities to resources (#7311) +* [04414e4fc](https://github.com/api-platform/core/commit/04414e4fc144244cf849ca122bfaacc638baa425) fix(serializer): improve #7270 by reducing inconsistencies (#7346) +* [2c06a22e2](https://github.com/api-platform/core/commit/2c06a22e244e5b0683558589a7ed2d7dd34a16a2) fix(validation): moving dependency from require-dev to require (#7296) +* [2cde06246](https://github.com/api-platform/core/commit/2cde06246cd593ad094de9c8f0ad1b178608f275) fix(openapi): output `partial` query parameter to OpenAPI when `pagination_client_enabled` is true (#7295) +* [2e6911c35](https://github.com/api-platform/core/commit/2e6911c359ef750edbccda9f89c14edd29bdca4d) fix(openapi): sync typehints between properties and getter/canner for alllowReserved and allowEmptyValue (#7322) +* [385953a92](https://github.com/api-platform/core/commit/385953a920d8b8c1c5e644b25d28b211c5ba7008) fix(jsonapi): handle type error when handling validation errors (#7330) +* [4f717c1e1](https://github.com/api-platform/core/commit/4f717c1e1aee2edb7bded054aacf31b52df30f27) fix(openapi): allow null on allowReserved and allowEmptyValue properties (#7315) +* [6bc112193](https://github.com/api-platform/core/commit/6bc11219320315af1a811ab49a4c451498f75430) fix(metadata): do not fail if phpstan/phpdoc-parser is missing (#7279) +* [871e5d3e1](https://github.com/api-platform/core/commit/871e5d3e1916e0d8bd1b4e1c4d983cd270a0922f) fix(symfony): restore graphql_playground option (#7274) +* [c41a0bca4](https://github.com/api-platform/core/commit/c41a0bca49778663743a52e9da9ddb3b1439c2f8) fix(jsonld): child class @src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php shortName (#7312) +* [c46c57918](https://github.com/api-platform/core/commit/c46c5791841f909af10faf1dd756eed772b6dc15) fix(doctrine): do not consider empty string as a current date (#7291) +* [d06b1a0a0](https://github.com/api-platform/core/commit/d06b1a0a0e9dcde7fcdb585b435cd617620b098b) fix(state): object-mapper reuse related entity (#7300) +* [d1073bc67](https://github.com/api-platform/core/commit/d1073bc67ca06b1abe6563c90efcbff4cfe050e4) fix(laravel): read property type before serialization (#7332) +* [d1abfc0fa](https://github.com/api-platform/core/commit/d1abfc0faf6dcef5d364ff719bdfd97e15275a28) fix(openapi): nullable default values in operation openapi definition (#7321) +* [d1e6772e3](https://github.com/api-platform/core/commit/d1e6772e32f632a5612f79ec58cad874af938694) fix(validation): property path on deepObject style (#7179) +* [d35e46b14](https://github.com/api-platform/core/commit/d35e46b1426063dbed4b59d1f07dbbde398a390e) fix(hydra): "property" may not be defined (#7293) +* [e7502b65a](https://github.com/api-platform/core/commit/e7502b65a12608d0926129a2dfc93a5d6f11fd01) fix(serializer): nested denormalization when allow_extra_attributes=false (#7270) +* [ecb3f6cef](https://github.com/api-platform/core/commit/ecb3f6cefcf87a0e040cb528a6cb2b31ca97663a) fix(httpcache): only map entites that are persisted +* [f3a54a239](https://github.com/api-platform/core/commit/f3a54a239e21c18716967e579d2cd2120a52ece1) fix: json formatted resource should not get xml errors #7287 (#7297) +* [0411cd9cc](https://github.com/api-platform/core/commit/0411cd9cc73f085a1918a13a3798e0c5aee5bbff) fix(jsonld): duplicate error fields when prefix is enabled (#7074) +* [073c0e282](https://github.com/api-platform/core/commit/073c0e2822c9d93d3303ee64178bed19e6e61080) !fix(openapi): allowReserved, allowEmtpyValue defaults to null +* [76c80a67c](https://github.com/api-platform/core/commit/76c80a67cdaf9615c598bf0fa3e37a89ce7589ba) fix: command name deprecation +* [79edced67](https://github.com/api-platform/core/commit/79edced67ccca1a7b80455dd94203501d9c4fa89) !fix(json-schema): share invariable sub-schemas +* [cff61eab8](https://github.com/api-platform/core/commit/cff61eab8643f8ed08d59c0684e77740d0d81b04) fix(metadata): append php file resource extractor (#7193) +* [f3d4afe03](https://github.com/api-platform/core/commit/f3d4afe032385f3b665131a365e42706930f0730) fix(symfony): validator type-info + +### Features + +* [092a3a4f8](https://github.com/api-platform/core/commit/092a3a4f89563109b3dac18351da25051f7aa745) feat(mongodb): partial paginator (#7352) +* [26d2394b7](https://github.com/api-platform/core/commit/26d2394b70342f99bb8a4699529f7a5c113830bb) feat(doctrine): new search filters (#7121) +* [1806cfae6](https://github.com/api-platform/core/commit/1806cfae683f907b6800852e6b2c7bc9ffb59fc1) feat(symfony): stop watch system provider/processor +* [2d501b315](https://github.com/api-platform/core/commit/2d501b315e2dfacf5fb5c83e136b09c56bdd2b7e) feat(laravel): support composite identifiers within `Link` (#7342) +* [6491bfc7a](https://github.com/api-platform/core/commit/6491bfc7a88af5722a98645ff648a461c9990b7e) feat(doctrine): improve http cache invalidation using the info from the mapping (#7319) +* [77b292bfe](https://github.com/api-platform/core/commit/77b292bfefcf655b7ade95bd23046dff5c38c464) feat(metadata): class is now class-string (#7307) +* [9e9cf648d](https://github.com/api-platform/core/commit/9e9cf648d6cbe21f9c7b07d54e797ac08ffc45eb) feat(metadata): introduce metadata mutators for resource & operations (#7213) +* [a8b82172a](https://github.com/api-platform/core/commit/a8b82172afa48f04438786aa13d00a33eb8956e1) feat(doctrine): support integer-backed enums in BackedEnumFilter (#7129) +* [b52187d91](https://github.com/api-platform/core/commit/b52187d91eca6cf08d60dee80d19d42516a7e8b7) feat(serializer): handle defaultType for DiscriminatorMap (#7284) +* [de9e7f576](https://github.com/api-platform/core/commit/de9e7f576039d9943f7e9b70e41886d3a9406655) feat(hal): allow to output null links for hal+json +* [f25d7d1a6](https://github.com/api-platform/core/commit/f25d7d1a6e0ed3fb60a01ccda60729721a82d215) feat(metadata): add setter for description (#7329) +* [1290ebb88](https://github.com/api-platform/core/commit/1290ebb88680dde94e536dfc19d9b330612e0909) feat(serializer): ability to throw access denied exception when denormalizing secured properties (#7221) +* [1862d03b7](https://github.com/api-platform/core/commit/1862d03b77c941cac13ed7bcbf1d555d50614b4f) feat(symfony): add error classes options to open api config (#7143) +* [202c60fcb](https://github.com/api-platform/core/commit/202c60fcb05be41bbe0bf03a3314ab3b7e9fed64) feat(openapi): license identifier (#7141) +* [24a1cf5a2](https://github.com/api-platform/core/commit/24a1cf5a2f9d56df791abdb625ec83594ee4f0f9) feat(elasticsearch): add support for v9 (#7180) +* [2a13100f4](https://github.com/api-platform/core/commit/2a13100f4edeb02191a3da891d99c1d054710166) feat(serializer): (un)set object-to-populate through denormalization context (#7124) +* [3ab0b8d0f](https://github.com/api-platform/core/commit/3ab0b8d0fba6a56014752d90a007f57483502551) feat(metadata): use PHP file as resource format +* [4abf17b3c](https://github.com/api-platform/core/commit/4abf17b3c49491ee1f77f951028422dacae90456) feat(symfony): CSS Color Schema Restriction for Property Validation (#7215) +* [4b1b94cad](https://github.com/api-platform/core/commit/4b1b94cad1bdd2e9c3d94cc22b7e3519a0d6c609) feat(symfony): autoconfigure classes using `#[ApiResource]` attribute (#6943) +* [bf09616cf](https://github.com/api-platform/core/commit/bf09616cfbbc2c19a9d04a7be3edc8a9bc0c07ef) feat(httpcache): add more cache directives to AddHeadersProcessor (#7008) + +JSON Streamer: + +* [ceeaf51e8](https://github.com/api-platform/core/commit/ceeaf51e806e772c2801cfaaa1286fc081a10a3e) feat: json streamer (#7225) + +Object Mapper: + +* [a42034dc3](https://github.com/api-platform/core/commit/a42034dc384ab1b372ea5bf4452db37791c3b739) feat(symfony): object mapper with state options (#6801) + +TypeInfo: + +* [7c796de0d](https://github.com/api-platform/core/commit/7c796de0d8d0dc6dd7c4d7a3da0afb61823b6258) feat(serializer): type info (#7104) +* [350d6d707](https://github.com/api-platform/core/commit/350d6d707a95587679b13c2a8670206b480d9ab2) feat(hydra): use `TypeInfo`'s `Type` (#7099) +* [55461a83c](https://github.com/api-platform/core/commit/55461a83cbb804fb73167d5196337b6a04e38c22) feat: Use `Type` of `TypeInfo` instead of `PropertyInfo` (#6979) +* [827e1f725](https://github.com/api-platform/core/commit/827e1f725b5f2a560cd0aaf9b63e7e7e8a9b329d) feat(elasticsearch): use `TypeInfo` type (#7098) +* [a1952e304](https://github.com/api-platform/core/commit/a1952e30403c2ccf98d5cffb667720c3f4c753bf) feat(openapi): use `TypeInfo`'s `Type` (#7096) +* [dd3356824](https://github.com/api-platform/core/commit/dd33568245de962ff99efcd8079d2c6f8daf8061) feat(laravel): use `TypeInfo`'s `Type` (#7101) +* [143d512ef](https://github.com/api-platform/core/commit/143d512ef24b988a9b80b3da28c9a36e4b346c76) feat(jsonapi): use `TypeInfo`'s `Type` (#7100) +* [1d579d095](https://github.com/api-platform/core/commit/1d579d095616a6025d63526f14402eaef173346b) feat(hal): use `TypeInfo` type (#7097) + +## v4.2.0-alpha.2 + +### Bug fixes + +* [02a764950](https://github.com/api-platform/core/commit/02a7649509f3f69abb74c94805707b0c253521fb) fix(symfony): explicitly set the target class when mapping entities to resources (#7311) +* [04414e4fc](https://github.com/api-platform/core/commit/04414e4fc144244cf849ca122bfaacc638baa425) fix(serializer): improve #7270 by reducing inconsistencies (#7346) +* [2c06a22e2](https://github.com/api-platform/core/commit/2c06a22e244e5b0683558589a7ed2d7dd34a16a2) fix(validation): moving dependency from require-dev to require (#7296) +* [2cde06246](https://github.com/api-platform/core/commit/2cde06246cd593ad094de9c8f0ad1b178608f275) fix(openapi): output `partial` query parameter to OpenAPI when `pagination_client_enabled` is true (#7295) +* [2e6911c35](https://github.com/api-platform/core/commit/2e6911c359ef750edbccda9f89c14edd29bdca4d) fix(openapi): sync typehints between properties and getter/canner for alllowReserved and allowEmptyValue (#7322) +* [385953a92](https://github.com/api-platform/core/commit/385953a920d8b8c1c5e644b25d28b211c5ba7008) fix(jsonapi): handle type error when handling validation errors (#7330) +* [4f717c1e1](https://github.com/api-platform/core/commit/4f717c1e1aee2edb7bded054aacf31b52df30f27) fix(openapi): allow null on allowReserved and allowEmptyValue properties (#7315) +* [6bc112193](https://github.com/api-platform/core/commit/6bc11219320315af1a811ab49a4c451498f75430) fix(metadata): do not fail if phpstan/phpdoc-parser is missing (#7279) +* [871e5d3e1](https://github.com/api-platform/core/commit/871e5d3e1916e0d8bd1b4e1c4d983cd270a0922f) fix(symfony): restore graphql_playground option (#7274) +* [c41a0bca4](https://github.com/api-platform/core/commit/c41a0bca49778663743a52e9da9ddb3b1439c2f8) fix(jsonld): child class @src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php shortName (#7312) +* [c46c57918](https://github.com/api-platform/core/commit/c46c5791841f909af10faf1dd756eed772b6dc15) fix(doctrine): do not consider empty string as a current date (#7291) +* [d06b1a0a0](https://github.com/api-platform/core/commit/d06b1a0a0e9dcde7fcdb585b435cd617620b098b) fix(state): object-mapper reuse related entity (#7300) +* [d1073bc67](https://github.com/api-platform/core/commit/d1073bc67ca06b1abe6563c90efcbff4cfe050e4) fix(laravel): read property type before serialization (#7332) +* [d1abfc0fa](https://github.com/api-platform/core/commit/d1abfc0faf6dcef5d364ff719bdfd97e15275a28) fix(openapi): nullable default values in operation openapi definition (#7321) +* [d1e6772e3](https://github.com/api-platform/core/commit/d1e6772e32f632a5612f79ec58cad874af938694) fix(validation): property path on deepObject style (#7179) +* [d35e46b14](https://github.com/api-platform/core/commit/d35e46b1426063dbed4b59d1f07dbbde398a390e) fix(hydra): "property" may not be defined (#7293) +* [e7502b65a](https://github.com/api-platform/core/commit/e7502b65a12608d0926129a2dfc93a5d6f11fd01) fix(serializer): nested denormalization when allow_extra_attributes=false (#7270) +* [ecb3f6cef](https://github.com/api-platform/core/commit/ecb3f6cefcf87a0e040cb528a6cb2b31ca97663a) fix(httpcache): only map entites that are persisted +* [f3a54a239](https://github.com/api-platform/core/commit/f3a54a239e21c18716967e579d2cd2120a52ece1) fix: json formatted resource should not get xml errors #7287 (#7297) +* [0411cd9cc](https://github.com/api-platform/core/commit/0411cd9cc73f085a1918a13a3798e0c5aee5bbff) fix(jsonld): duplicate error fields when prefix is enabled (#7074) +* [073c0e282](https://github.com/api-platform/core/commit/073c0e2822c9d93d3303ee64178bed19e6e61080) !fix(openapi): allowReserved, allowEmtpyValue defaults to null +* [76c80a67c](https://github.com/api-platform/core/commit/76c80a67cdaf9615c598bf0fa3e37a89ce7589ba) fix: command name deprecation +* [79edced67](https://github.com/api-platform/core/commit/79edced67ccca1a7b80455dd94203501d9c4fa89) !fix(json-schema): share invariable sub-schemas +* [cff61eab8](https://github.com/api-platform/core/commit/cff61eab8643f8ed08d59c0684e77740d0d81b04) fix(metadata): append php file resource extractor (#7193) +* [f3d4afe03](https://github.com/api-platform/core/commit/f3d4afe032385f3b665131a365e42706930f0730) fix(symfony): validator type-info + +### Features + +* [1806cfae6](https://github.com/api-platform/core/commit/1806cfae683f907b6800852e6b2c7bc9ffb59fc1) feat(symfony): stop watch system provider/processor +* [2d501b315](https://github.com/api-platform/core/commit/2d501b315e2dfacf5fb5c83e136b09c56bdd2b7e) feat(laravel): support composite identifiers within `Link` (#7342) +* [6491bfc7a](https://github.com/api-platform/core/commit/6491bfc7a88af5722a98645ff648a461c9990b7e) feat(doctrine): improve http cache invalidation using the info from the mapping (#7319) +* [77b292bfe](https://github.com/api-platform/core/commit/77b292bfefcf655b7ade95bd23046dff5c38c464) feat(metadata): class is now class-string (#7307) +* [9e9cf648d](https://github.com/api-platform/core/commit/9e9cf648d6cbe21f9c7b07d54e797ac08ffc45eb) feat(metadata): introduce metadata mutators for resource & operations (#7213) +* [a8b82172a](https://github.com/api-platform/core/commit/a8b82172afa48f04438786aa13d00a33eb8956e1) feat(doctrine): support integer-backed enums in BackedEnumFilter (#7129) +* [b52187d91](https://github.com/api-platform/core/commit/b52187d91eca6cf08d60dee80d19d42516a7e8b7) feat(serializer): handle defaultType for DiscriminatorMap (#7284) +* [de9e7f576](https://github.com/api-platform/core/commit/de9e7f576039d9943f7e9b70e41886d3a9406655) feat(hal): allow to output null links for hal+json +* [f25d7d1a6](https://github.com/api-platform/core/commit/f25d7d1a6e0ed3fb60a01ccda60729721a82d215) feat(metadata): add setter for description (#7329) +* [1290ebb88](https://github.com/api-platform/core/commit/1290ebb88680dde94e536dfc19d9b330612e0909) feat(serializer): ability to throw access denied exception when denormalizing secured properties (#7221) +* [1862d03b7](https://github.com/api-platform/core/commit/1862d03b77c941cac13ed7bcbf1d555d50614b4f) feat(symfony): add error classes options to open api config (#7143) +* [202c60fcb](https://github.com/api-platform/core/commit/202c60fcb05be41bbe0bf03a3314ab3b7e9fed64) feat(openapi): license identifier (#7141) +* [24a1cf5a2](https://github.com/api-platform/core/commit/24a1cf5a2f9d56df791abdb625ec83594ee4f0f9) feat(elasticsearch): add support for v9 (#7180) +* [2a13100f4](https://github.com/api-platform/core/commit/2a13100f4edeb02191a3da891d99c1d054710166) feat(serializer): (un)set object-to-populate through denormalization context (#7124) +* [3ab0b8d0f](https://github.com/api-platform/core/commit/3ab0b8d0fba6a56014752d90a007f57483502551) feat(metadata): use PHP file as resource format +* [4abf17b3c](https://github.com/api-platform/core/commit/4abf17b3c49491ee1f77f951028422dacae90456) feat(symfony): CSS Color Schema Restriction for Property Validation (#7215) +* [4b1b94cad](https://github.com/api-platform/core/commit/4b1b94cad1bdd2e9c3d94cc22b7e3519a0d6c609) feat(symfony): autoconfigure classes using `#[ApiResource]` attribute (#6943) +* [bf09616cf](https://github.com/api-platform/core/commit/bf09616cfbbc2c19a9d04a7be3edc8a9bc0c07ef) feat(httpcache): add more cache directives to AddHeadersProcessor (#7008) + +JSON Streamer: + +* [ceeaf51e8](https://github.com/api-platform/core/commit/ceeaf51e806e772c2801cfaaa1286fc081a10a3e) feat: json streamer (#7225) + +Object Mapper: + +* [a42034dc3](https://github.com/api-platform/core/commit/a42034dc384ab1b372ea5bf4452db37791c3b739) feat(symfony): object mapper with state options (#6801) + +TypeInfo: + +* [7c796de0d](https://github.com/api-platform/core/commit/7c796de0d8d0dc6dd7c4d7a3da0afb61823b6258) feat(serializer): type info (#7104) +* [350d6d707](https://github.com/api-platform/core/commit/350d6d707a95587679b13c2a8670206b480d9ab2) feat(hydra): use `TypeInfo`'s `Type` (#7099) +* [55461a83c](https://github.com/api-platform/core/commit/55461a83cbb804fb73167d5196337b6a04e38c22) feat: Use `Type` of `TypeInfo` instead of `PropertyInfo` (#6979) +* [827e1f725](https://github.com/api-platform/core/commit/827e1f725b5f2a560cd0aaf9b63e7e7e8a9b329d) feat(elasticsearch): use `TypeInfo` type (#7098) +* [a1952e304](https://github.com/api-platform/core/commit/a1952e30403c2ccf98d5cffb667720c3f4c753bf) feat(openapi): use `TypeInfo`'s `Type` (#7096) +* [dd3356824](https://github.com/api-platform/core/commit/dd33568245de962ff99efcd8079d2c6f8daf8061) feat(laravel): use `TypeInfo`'s `Type` (#7101) +* [143d512ef](https://github.com/api-platform/core/commit/143d512ef24b988a9b80b3da28c9a36e4b346c76) feat(jsonapi): use `TypeInfo`'s `Type` (#7100) +* [1d579d095](https://github.com/api-platform/core/commit/1d579d095616a6025d63526f14402eaef173346b) feat(hal): use `TypeInfo` type (#7097) + +## v4.2.0-alpha.1 + +Introducing new Symfony components! + +### Features + +* [1290ebb88](https://github.com/api-platform/core/commit/1290ebb88680dde94e536dfc19d9b330612e0909) feat(serializer): ability to throw access denied exception when denormalizing secured properties (#7221) +* [1862d03b7](https://github.com/api-platform/core/commit/1862d03b77c941cac13ed7bcbf1d555d50614b4f) feat(symfony): add error classes options to open api config (#7143) +* [202c60fcb](https://github.com/api-platform/core/commit/202c60fcb05be41bbe0bf03a3314ab3b7e9fed64) feat(openapi): license identifier (#7141) +* [24a1cf5a2](https://github.com/api-platform/core/commit/24a1cf5a2f9d56df791abdb625ec83594ee4f0f9) feat(elasticsearch): add support for v9 (#7180) +* [2a13100f4](https://github.com/api-platform/core/commit/2a13100f4edeb02191a3da891d99c1d054710166) feat(serializer): (un)set object-to-populate through denormalization context (#7124) +* [3ab0b8d0f](https://github.com/api-platform/core/commit/3ab0b8d0fba6a56014752d90a007f57483502551) feat(metadata): use PHP file as resource format +* [4abf17b3c](https://github.com/api-platform/core/commit/4abf17b3c49491ee1f77f951028422dacae90456) feat(symfony): CSS Color Schema Restriction for Property Validation (#7215) +* [4b1b94cad](https://github.com/api-platform/core/commit/4b1b94cad1bdd2e9c3d94cc22b7e3519a0d6c609) feat(symfony): autoconfigure classes using `#[ApiResource]` attribute (#6943) +* [bf09616cf](https://github.com/api-platform/core/commit/bf09616cfbbc2c19a9d04a7be3edc8a9bc0c07ef) feat(httpcache): add more cache directives to AddHeadersProcessor (#7008) + +Object Mapper: + +* [a42034dc3](https://github.com/api-platform/core/commit/a42034dc384ab1b372ea5bf4452db37791c3b739) feat(symfony): object mapper with state options (#6801) + +TypeInfo: + +* [7c796de0d](https://github.com/api-platform/core/commit/7c796de0d8d0dc6dd7c4d7a3da0afb61823b6258) feat(serializer): type info (#7104) +* [350d6d707](https://github.com/api-platform/core/commit/350d6d707a95587679b13c2a8670206b480d9ab2) feat(hydra): use `TypeInfo`'s `Type` (#7099) +* [55461a83c](https://github.com/api-platform/core/commit/55461a83cbb804fb73167d5196337b6a04e38c22) feat: Use `Type` of `TypeInfo` instead of `PropertyInfo` (#6979) +* [827e1f725](https://github.com/api-platform/core/commit/827e1f725b5f2a560cd0aaf9b63e7e7e8a9b329d) feat(elasticsearch): use `TypeInfo` type (#7098) +* [a1952e304](https://github.com/api-platform/core/commit/a1952e30403c2ccf98d5cffb667720c3f4c753bf) feat(openapi): use `TypeInfo`'s `Type` (#7096) +* [dd3356824](https://github.com/api-platform/core/commit/dd33568245de962ff99efcd8079d2c6f8daf8061) feat(laravel): use `TypeInfo`'s `Type` (#7101) +* [143d512ef](https://github.com/api-platform/core/commit/143d512ef24b988a9b80b3da28c9a36e4b346c76) feat(jsonapi): use `TypeInfo`'s `Type` (#7100) +* [1d579d095](https://github.com/api-platform/core/commit/1d579d095616a6025d63526f14402eaef173346b) feat(hal): use `TypeInfo` type (#7097) + +### Bug fixes + +* [0411cd9cc](https://github.com/api-platform/core/commit/0411cd9cc73f085a1918a13a3798e0c5aee5bbff) fix(jsonld): duplicate error fields when prefix is enabled (#7074) +* [073c0e282](https://github.com/api-platform/core/commit/073c0e2822c9d93d3303ee64178bed19e6e61080) !fix(openapi): allowReserved, allowEmtpyValue defaults to null +* [76c80a67c](https://github.com/api-platform/core/commit/76c80a67cdaf9615c598bf0fa3e37a89ce7589ba) fix: command name deprecation +* [79edced67](https://github.com/api-platform/core/commit/79edced67ccca1a7b80455dd94203501d9c4fa89) !fix(json-schema): share invariable sub-schemas +* [cff61eab8](https://github.com/api-platform/core/commit/cff61eab8643f8ed08d59c0684e77740d0d81b04) fix(metadata): append php file resource extractor (#7193) +* [f3d4afe03](https://github.com/api-platform/core/commit/f3d4afe032385f3b665131a365e42706930f0730) fix(symfony): validator type-info + +## v4.1.25 + +### Bug fixes + +* [54352d853](https://github.com/api-platform/core/commit/54352d853b219327ed58d40e1f54c46f2193c1a9) fix(jsonld): hydra context w3.org updates (#7410) +* [85f198a56](https://github.com/api-platform/core/commit/85f198a5618d5e955b2fbcfb079fe7286af05413) fix(laravel): filter should query correct property +* [91b17f1f3](https://github.com/api-platform/core/commit/91b17f1f3731339023910f795d686111a8941920) fix(laravel): correct the example of swagger_ui.apiKeys config (#7392) + +## v4.1.24 + +### Bug fixes + +* [07112a27d](https://github.com/api-platform/core/commit/07112a27d89c6099ac56e152d61fd434015f4f5a) fix(laravel): remove duplication code exception +* [4abec444e](https://github.com/api-platform/core/commit/4abec444e80d8c6d0925a1b4159006993980305b) fix(metadata): compute isWritable during updates (#7383) +* [7c117d9f7](https://github.com/api-platform/core/commit/7c117d9f758a11fcea1034983e04c40505eb9924) fix: phpdoc for HttpOperation paginationViaCursor parameter (#7379) + +### Features + +Laravel compatibility with `api-platform/http-cache`: + +* [fa2bc95ff](https://github.com/api-platform/core/commit/fa2bc95ff87e00262fa5e28ad4f06d4c5a2c912e) fix(laravel): http cache compatibility (#7380) + +## v4.1.23 + +### Bug fixes + +* [254cbdd00](https://github.com/api-platform/core/commit/254cbdd0062ade7f3b2d7745e90cc309f8b9b627) fix(symfony): only set name_converter for the default serializer (#6101) (#7365) +* [dcbc529a0](https://github.com/api-platform/core/commit/dcbc529a0e2d65218b227f0ece46972402726cfd) fix: pagination via cursor on ApiResource operations (#7368) + +## v4.1.22 + +### Bug fixes + +* [11bba62c2](https://github.com/api-platform/core/commit/11bba62c2aa0f60b09955f7cf55d7135938c0e48) fix(laravel): serialization issue with camelCase relation (#7356) + +## v4.1.21 + +### Bug fixes + +* [04414e4fc](https://github.com/api-platform/core/commit/04414e4fc144244cf849ca122bfaacc638baa425) fix(serializer): improve #7270 by reducing inconsistencies (#7346) +* [2e6911c35](https://github.com/api-platform/core/commit/2e6911c359ef750edbccda9f89c14edd29bdca4d) fix(openapi): sync typehints between properties and getter/canner for alllowReserved and allowEmptyValue (#7322) +* [385953a92](https://github.com/api-platform/core/commit/385953a920d8b8c1c5e644b25d28b211c5ba7008) fix(jsonapi): handle type error when handling validation errors (#7330) +* [4f717c1e1](https://github.com/api-platform/core/commit/4f717c1e1aee2edb7bded054aacf31b52df30f27) fix(openapi): allow null on allowReserved and allowEmptyValue properties (#7315) +* [c46c57918](https://github.com/api-platform/core/commit/c46c5791841f909af10faf1dd756eed772b6dc15) fix(doctrine): do not consider empty string as a current date (#7291) +* [d1073bc67](https://github.com/api-platform/core/commit/d1073bc67ca06b1abe6563c90efcbff4cfe050e4) fix(laravel): read property type before serialization (#7332) +* [d1abfc0fa](https://github.com/api-platform/core/commit/d1abfc0faf6dcef5d364ff719bdfd97e15275a28) fix(openapi): nullable default values in operation openapi definition (#7321) +* [e7502b65a](https://github.com/api-platform/core/commit/e7502b65a12608d0926129a2dfc93a5d6f11fd01) fix(serializer): nested denormalization when allow_extra_attributes=false (#7270) + +### Features + +* [2d501b315](https://github.com/api-platform/core/commit/2d501b315e2dfacf5fb5c83e136b09c56bdd2b7e) feat(laravel): support composite identifiers within `Link` (#7342) + +## v4.1.20 + +### Bug fixes + +* [c41a0bca4](https://github.com/api-platform/core/commit/c41a0bca49778663743a52e9da9ddb3b1439c2f8) fix(jsonld): child class @type shortName (#7312) +* [d26088ba1](https://github.com/api-platform/core/commit/d26088ba1080f9fb8d94db5b476b0322d2a14ab2) chore: allow doctrine-persistence:^4.0 (#7309) + +## v4.1.19 + +### Bug fixes + +* [2c06a22e2](https://github.com/api-platform/core/commit/2c06a22e244e5b0683558589a7ed2d7dd34a16a2) fix(validation): moving dependency from require-dev to require (#7296) +* [2cde06246](https://github.com/api-platform/core/commit/2cde06246cd593ad094de9c8f0ad1b178608f275) fix(openapi): output `partial` query parameter to OpenAPI when `pagination_client_enabled` is true (#7295) +* [6bc112193](https://github.com/api-platform/core/commit/6bc11219320315af1a811ab49a4c451498f75430) fix(metadata): do not fail if phpstan/phpdoc-parser is missing (#7279) +* [871e5d3e1](https://github.com/api-platform/core/commit/871e5d3e1916e0d8bd1b4e1c4d983cd270a0922f) fix(symfony): restore graphql_playground option (#7274) +* [d1e6772e3](https://github.com/api-platform/core/commit/d1e6772e32f632a5612f79ec58cad874af938694) fix(validation): property path on deepObject style (#7179) +* [d35e46b14](https://github.com/api-platform/core/commit/d35e46b1426063dbed4b59d1f07dbbde398a390e) fix(hydra): "property" may not be defined (#7293) +* [f3a54a239](https://github.com/api-platform/core/commit/f3a54a239e21c18716967e579d2cd2120a52ece1) fix: json formatted resource should not get xml errors #7287 (#7297) + +## v4.1.18 + +### Bug fixes + +* [0e712d969](https://github.com/api-platform/core/commit/0e712d9692f018b9816764ada9fa4c7b02b5e028) fix(symfony): catch InvalidUriVariableException in IriConverter (#7271) +* [221e5d97f](https://github.com/api-platform/core/commit/221e5d97f9e1114d642c834e29392a35967a9960) fix(openapi): command options mode (`InputOption::VALUE_REQUIRED`) (#7266) +* [3ed6f30f5](https://github.com/api-platform/core/commit/3ed6f30f5ce60ac45a1a1df6bac9372a7b712d51) fix(symfony): fix resolving groups in ValidationGroupsExtractorTrait when GroupSequence (#7272) +* [6e4e46bfe](https://github.com/api-platform/core/commit/6e4e46bfef0aaa87b2bdc56a1b28510fcdaa2914) fix(json-schema): Rebuild sub-schema definition without `@id` property when `genId` is `false` (#7162) (#7251) +* [803165228](https://github.com/api-platform/core/commit/80316522807be2af25657f2936341ce2f527c364) fix(symfony): missing GroupSequence type for validation groups +* [85e18a2d6](https://github.com/api-platform/core/commit/85e18a2d665baf791027a93106d6a6c293464883) fix(metadata): support stateOptions in YAML and XML for Doctrine ORM/ODM (#7217) +* [88e0073bf](https://github.com/api-platform/core/commit/88e0073bf2eb9c27a35d6a04a32919908cdae704) fix(validator): error xml format output +* [bf271d139](https://github.com/api-platform/core/commit/bf271d139aa6cd6c74d2dd0ee5a482d03a1afca4) fix(validator): parameter validation list|string (#7245) +* [d3e73f09a](https://github.com/api-platform/core/commit/d3e73f09afd6bde5763e0c2c7a4a0203f857d7c3) fix(metadata): read every OpenApiOperation attributes in Xml instead of only "deprecated" (#7189) +* [e40bb19a6](https://github.com/api-platform/core/commit/e40bb19a6d80d60e5f06ac02b5bbf1aaac08e46a) fix(laravel): decorate error handler (#7247) +* [f0604a07e](https://github.com/api-platform/core/commit/f0604a07e79ba2ee6a47cee20fcdaebbb68bbcaf) fix: getcontainer return type (#7230) +* [f46c0a3f4](https://github.com/api-platform/core/commit/f46c0a3f47265d48d2863860d0e274f83367c929) fix(jsonld): reset gen_id configuration (#7264) +* [f82464431](https://github.com/api-platform/core/commit/f8246443134b07fe6a2b591642af1ed3a5f40b44) fix(state): depend only on translation contracts (#7262) +* [fa37c1eba](https://github.com/api-platform/core/commit/fa37c1eba871c533c2e3436a915d0f02dd2baa78) fix(state): error xml format output (#7273) + +### Experimental Features + +* [8885000db](https://github.com/api-platform/core/commit/8885000dbfa610146ff8f3187d41a0dde9b22e26) feat(state): cast parameter values to validate with the Type constraint (#7240) + +## v4.1.17 + +### Bug fixes + +* [34a0d54b1](https://github.com/api-platform/core/commit/34a0d54b1fe195a233726ff2e8304c29f43954ca) fix(jsonld): genId false should work with embeded resources (#7219) +* [c025b8f9f](https://github.com/api-platform/core/commit/c025b8f9f8ab51489dc75e470c057d6fae879fa0) fix(openapi): correct example usage for 3.0 (#7218) +* [cdda4146b](https://github.com/api-platform/core/commit/cdda4146b429f8e605504e1dc403faa41f0255a7) fix(laravel): Allow `LinksHandler` to handle polymorphic relationships (#7231) +* [dba97370f](https://github.com/api-platform/core/commit/dba97370fb8996cf4fc1d5b6d8ca92faf4e68637) fix(metadata): boolean type detection from parameter's schema (#7223) + +## v4.1.16 + +### Bug fixes + +* [4a99a5f6e](https://github.com/api-platform/core/commit/4a99a5f6e7eb13e9e77872dc33ec5b3dc2deef25) fix(laravel): persist HasMany and MorphMany relationships (#7208) +* [9bfa5fef0](https://github.com/api-platform/core/commit/9bfa5fef0dfa129078118d0ce67f1ec2c8d548d7) fix(metadata): call dynamic validation groups #7184 (#7184) +* [b2131645d](https://github.com/api-platform/core/commit/b2131645dec11a5dbf8bbe507f578720a84a686e) fix(laravel): route where uses requirements (#7199) +* [b2b5f99c6](https://github.com/api-platform/core/commit/b2b5f99c64ced9f3580fdaee099d0e34e75d2697) refactor(state): merge parameter and link security (#7200) + +Notes: + +Two providers are now available on parameters (query parameters, header and uri variables `Link`): + +- `ReadLinkParameterProvider` previously used for link security (renamed from `Symfony\Security\State\LinkedReadProvider`) +- `IriConverterParameterProvider` this allows you to read a resource from an IRI usefull for filters (eg `?author=/authors/1`) + +Previous tests on link security were left untouched we removed the experimental class `Symfony\Security\State\LinkAccessCheckerProvider` as well as the `LinkedReadProvider` as they're not used anymore. + + +## v4.1.15 - v4.1.14 - v4.1.13 + +There was an issue with the subtree split as we attempted to test lower dependencies on the subtree split, some components where wrongly tagged. + +The proper fix is at: https://github.com/api-platform/core/pull/7196 + +### Bug fixes + +* [4bdfd6063](https://github.com/api-platform/core/commit/4bdfd6063a6e80fc9cc33ff16c0ec98fec688291) fix(symfony, laravel) skip webhooks when generates routing (#7175) +* [582076cb6](https://github.com/api-platform/core/commit/582076cb6c0dbcde205fc49d97a0f50405544d0b) fix(openapi): duplicate get path for webhooks (#7174) +* [84b967930](https://github.com/api-platform/core/commit/84b96793043c939f333c1aa2e96f69b23d3e9ece) fix(laravel): persist morph relations (#7170) +* [d51cddba2](https://github.com/api-platform/core/commit/d51cddba2a7618c08e511582ac2f41a6b0eaf657) fix(laravel): index on non-primary key (#7183) +* [fdb485dd2](https://github.com/api-platform/core/commit/fdb485dd2af56f7664bd90705d81e8f58c8b494a) fix(openapi): `example` and `default` with nullable value not being shown + +### Features + +* [79d08db6a](https://github.com/api-platform/core/commit/79d08db6a4728303b37440c610c96a490d44780a) feat(elasticsearch): add support for v9 (#7180) + +## v4.1.11 - v4.1.12 + +### Bug fixes + +* [b14a463a9](https://github.com/api-platform/core/commit/b14a463a9da8a285eba1b0adc63ca4121efb0dce) fix(laravel): register error handler without graphql + +## v4.1.10 + +### Bug fixes + +* [329acf21e](https://github.com/api-platform/core/commit/329acf21e3a8618136a21b9121c5891f1fe6b9e8) fix(metadata): infer parameter string type from schema (#7161) +* [5459ba375](https://github.com/api-platform/core/commit/5459ba375b0e7ffd1c783a6e18a6452769eaff46) fix(metadata): parameter cast to array flag (#7160) +* [fe73002bf](https://github.com/api-platform/core/commit/fe73002bf5ae64adb1eb9e310dc62ff158de094d) fix(laravel): duplicated property names +* [b0390080e](https://github.com/api-platform/core/commit/b0390080e90be9ac494c8b4d968e59a4962f32ca) fix(laravel): name convert validation property path +* [730d17a30](https://github.com/api-platform/core/commit/730d17a306df4b92082484a19e82c5e150537331) fix(laravel): validate the model instead of body +* [4d66f5ef3](https://github.com/api-platform/core/commit/4d66f5ef313fe3857611e8345702a10019c79ec5) fix(laravel): persist embeded relations with groups +* [39123942a](https://github.com/api-platform/core/commit/39123942a0e1ff9dfb1e46f4a410160e3cd3fbd7) fix(laravel): register error handler without graphql +* [fd010ea1b](https://github.com/api-platform/core/commit/fd010ea1be86073f5d8905d5640846390ade7ce6) fix(openapi): nullable externalDocs return type +* [470c2e8bd](https://github.com/api-platform/core/commit/470c2e8bdf8d7a7502c02d2b681ced2001e2c1cc) fix(httpcache): iri cache tag for collection operation with path parameter +* [9c0dbb653](https://github.com/api-platform/core/commit/9c0dbb65319beb89193e653e14c99352ac529a55) fix(state): do not expose FQCN in DeserializeProvider on PartialDenormalizationException (#7158) +* [f78986000](https://github.com/api-platform/core/commit/f789860009888582791611a31d6084c620050615) fix(serializer): exception message to not expose resource FQCN (#7156) + +### Features + +* [767fa926b](https://github.com/api-platform/core/commit/767fa926b10bef771e896300b8e796287392d8c0) feat(laravel): add name_converter option + +## v4.1.9 + +### Bug fixes + +* [4dd0cdfc4](https://github.com/api-platform/core/commit/4dd0cdfc4a7f4cc73e7e67a49ee790ed1aaf5707) fix(doctrine): support integer-backed enums in BackedEnumFilter (#7127) +* [70a922573](https://github.com/api-platform/core/commit/70a9225737bece18ad4b66b8ac636c38b2a1008a) fix: error formats (#7148) +* [723d041c4](https://github.com/api-platform/core/commit/723d041c47467d8be0d8a0f20431da9de0a87c5a) fix(metadata): xml PHPize HTTP cache headers (#7140) +* [d3d0ca21b](https://github.com/api-platform/core/commit/d3d0ca21bd45a8383e3e51166a65162be93655bf) fix(serializer): invalid uri variable 400 response (#7135) + +## v4.1.8 + +### Bug fixes + +* [0cd9b9f70](https://github.com/api-platform/core/commit/0cd9b9f7002829a6238ed4a1731c3a123c4a91c4) fix: backport handling of union/intersection type in item normalizer (#7106) +* [0fc0904b9](https://github.com/api-platform/core/commit/0fc0904b9409e81041d4d5ae48d699f3360208fc) fix(metadata): correct class to exclude defaults (#7088) +* [5145a8383](https://github.com/api-platform/core/commit/5145a8383c8e234012b957a36701ec824774a817) fix: filtering not using strategy set in attribute when strategy not set per property (#7136) +* [613bb5b75](https://github.com/api-platform/core/commit/613bb5b75fe6cbc79c99e06bcbd664fc3f25c167) fix(doctrine): filters schema for dates and numbers (#7131) +* [ed1ef2594](https://github.com/api-platform/core/commit/ed1ef2594e72180e5fe153e762809e3c22b29e7f) fix(state): specify :property parameter properties (#7110) +* [f55606b01](https://github.com/api-platform/core/commit/f55606b01179703fb4be44f1b66a36136852f154) fix(serializer): throw NotNormalizableValueException when resource not found or invalid IRI on denormalization +* [3faf5d4b4](https://github.com/api-platform/core/commit/3faf5d4b44f7c39d7a9d6ecbcfdf90f7061f1b6f) fix(doctrine): order filter instance service +* [0ff950f37](https://github.com/api-platform/core/commit/0ff950f376345ff6a6d39a202b8bef15c3e423d2) fix: type info deprecations to baseline +* [d93d580a1](https://github.com/api-platform/core/commit/d93d580a1dbaf6a1af5f05f0ebde2f7b68d75d25) fix: command name deprecation + +## v4.1.7 + +### Bug fixes + +* [40c09d1c0](https://github.com/api-platform/core/commit/40c09d1c0560ec5c2a96391faf9fbe328f6f2f97) fix(metadata): yaml link security (#7066) +* [545eb69b8](https://github.com/api-platform/core/commit/545eb69b86797f436b8c494912513e1ef9064550) fix(symfony): internal resources should not inherit global defaults (#7073) +* [615bdd6b1](https://github.com/api-platform/core/commit/615bdd6b1c372f6438c6460dfcce69ef6c746e08) fix(hydra): use correctly enable_docs (#7062) +* [694681f4b](https://github.com/api-platform/core/commit/694681f4b187688dd41de1912637003a2fb313a2) fix(symfony): do not ignore Test files on symfony (#7077) +* [6f15be5c7](https://github.com/api-platform/core/commit/6f15be5c7badf197f10bb7d57dd79373f855ef20) fix(laravel): eloquent BelongsTo linking (#7075) +* [dddd3eee4](https://github.com/api-platform/core/commit/dddd3eee40af2e8126476ce3fe4c69f33ec15ac0) fix(metadata): parameter provider within filter (#7081) +* [dfca9bf60](https://github.com/api-platform/core/commit/dfca9bf6023c5535ce7ee9b3829a8efd7f1f9398) Revert "fix(jsonld): duplicate error fields when prefix is enabled (#7021)" (#7074) +* [d696a8e42](https://github.com/api-platform/core/commit/d696a8e4284b0e5b5c56300852bab70283386a99) fix(metadata): use template typehint in parameters (#7078) + +## v4.1.6 + +### Bug fixes + +* [44e560839](https://github.com/api-platform/core/commit/44e56083996f2f00a1d87a149fd41aeb0149e4dd) fix(laravel): undefined variable app + +## v4.1.5 + +### Bug fixes + +* [60747cc8c](https://github.com/api-platform/core/commit/60747cc8c2fb855798c923b5537888f8d0969568) fix(graphql): access to unauthorized resource using node Relay [CVE-2025-31481](https://github.com/api-platform/core/security/advisories/GHSA-cg3c-245w-728m) +* [7af65aad1](https://github.com/api-platform/core/commit/7af65aad13037d7649348ee3dcd88e084ef771f8) fix(graphql): property security might be cached w/ different objects [CVE-2025-31485](https://github.com/api-platform/core/security/advisories/GHSA-428q-q3vv-3fq3) + +## v4.1.4 + +### Bug fixes + +* [eaf007536](https://github.com/api-platform/core/commit/eaf00753693a493788f5ad60c4c45295048eb1a7) fix(state): support filter interface on serializer filter parameter provider (#7047) +* [b0fa2291e](https://github.com/api-platform/core/commit/b0fa2291e50a57789eb14777277769627931cfbd) fix(laravel): error handler only on api routes (#7049) +* [1eb9da13a](https://github.com/api-platform/core/commit/1eb9da13a7e06a8d380dfa27e23a0f8457ceb2bd) fix(laravel): missing filters (#7056) +* [a46a76889](https://github.com/api-platform/core/commit/a46a7688958428b76eb7b6181ae1636cd3b7a4bf) fix(symfony): serialization of closure inside the profiler (#7054) +* [c1b50e2cb](https://github.com/api-platform/core/commit/c1b50e2cb082b00a371bf62c7e86c2f3fea5dadf) fix(doctrine): joinColumn might be an array (#7060) +* [f4c426d71](https://github.com/api-platform/core/commit/f4c426d719b01debaa993b00d03cce8964057ecc) Revert "fix(doctrine): throw an exception when a filter is not found in a par…" (#7046) + +## v4.1.3 + +### Bug fixes + +* [8a2265041](https://github.com/api-platform/core/commit/8a22650419fd32efdafad43493f2327b38dd3ee6) fix(laravel): defer "filters" dependent services (#7045) + +## v4.1.2 + +### Bug fixes + +* [42f25cf04](https://github.com/api-platform/core/commit/42f25cf0461c382ce10e2378be5e9428c23eb94a) fix: react@18 for UMD + add Laravel support in update-js.sh (#7028) +* [77ffc380b](https://github.com/api-platform/core/commit/77ffc380bcd3c22a1eb4fb0c4f1cd265cfd1a898) fix(laravel): do not require tests while autoconfiguring (#7024) +* [935b9b55d](https://github.com/api-platform/core/commit/935b9b55da0eacce3dd2bbed4584b10b154d97c2) fix(laravel): json api default parameters (#7027) +* [a2824ff4b](https://github.com/api-platform/core/commit/a2824ff4be6276e37e37a3b4e4fb2e9a0096789c) fix(laravel): defer autoconfiguration (#7040) + + +## v4.1.1 + +### Bug fixes + +* [1e0bc9dc8](https://github.com/api-platform/core/commit/1e0bc9dc8ebe2f947ac0b3bc153047b0b28b0a17) fix(laravel): query extensions with item operations (#7001) +* [1e7076c65](https://github.com/api-platform/core/commit/1e7076c65aea9a2f09b0dcc6598ef2a705e0bb45) fix(laravel): register ErrorProvider (#7018) +* [2771363b0](https://github.com/api-platform/core/commit/2771363b03f1b1c27313ecd457f7a4934524151c) fix(validation): deprecate string message for ValidationException constructor (#7005) +* [500062da2](https://github.com/api-platform/core/commit/500062da2074585979e9a92939bc3b7a3c7554c5) fix(symfony): add a `alwaysBootKernel` property for BC layer (#7007) +* [8697d6630](https://github.com/api-platform/core/commit/8697d66304d276feeef6f838c8c54291d3563aab) fix(openapi): boolean "true" value in HttpOperation::openapi (#7003) +* [b1e0c889c](https://github.com/api-platform/core/commit/b1e0c889cc96602afc9c68fccb9da25a6b6fd354) fix(doctrine): correct the use statement for ManagerRegistry (#7004) +* [fcbd804b2](https://github.com/api-platform/core/commit/fcbd804b29907baba8878ca12ff013732e0326e4) fix(jsonld): duplicate error fields when prefix is enabled (#7021) + +### Features + +* [129853668](https://github.com/api-platform/core/commit/129853668c3fd66bcfe1a298a0f662c29545f7a4) feat(laravel): stateOptions modelClass for eloquent (#7020) +* [42991b941](https://github.com/api-platform/core/commit/42991b9418c340e44c9e30360e5dd4d869433859) feat(laravel): openapi export command (#7016) +* [dd1b89f9b](https://github.com/api-platform/core/commit/dd1b89f9b771544cb8449d7e9f8f4bbd80c615d2) feat(laravel): auto configure our tagged interfaces (#7014) + +Also contains [v4.0.20 changes](#v4020). + +## v4.1.0 + +### Bug fixes + +* [da37ca91c](https://github.com/api-platform/core/commit/da37ca91c83e4f715c5acbbe3dc0296e8055bebc) fix(laravel): default to file cache instead of cache.default (#6955) +* [e041d721e](https://github.com/api-platform/core/commit/e041d721ea6fe24026eb021905740b25102d7966) fix: errors retrieval and documentation (#6952) +* [e6130cbcc](https://github.com/api-platform/core/commit/e6130cbccc1a8963b731769ef34f39e09f5d2cde) fix: missing filters on swagger ui entrypoint (#6950) +* [01fd74268](https://github.com/api-platform/core/commit/01fd74268cf0e4a289b31ea74bea7f4e089b8361) fix(laravel): restore accidentally removed BooleanFilter +* [db40a63e7](https://github.com/api-platform/core/commit/db40a63e729fe03be84111dac2b0774fea4ab343) fix(hydra): rdfs:label should not duplicate title (#6748) +* [deb2ed265](https://github.com/api-platform/core/commit/deb2ed265dfee7b8a73fd3b542aef3e29eca3412) fix(laravel): fix use laravel fillable for writable props (#6898) +* [67fbe51c5](https://github.com/api-platform/core/commit/67fbe51c570abe1ece6651ae6a037662e9012881) fix: reintroduce the `show_webby` parameter in Laravel config (#6741) + +### Features + +* [cb5ede1c5](https://github.com/api-platform/core/commit/cb5ede1c5171677a90dd2d96a87e73cd549c0218) feat: add checkMode parameter to control json schema validation (#6974) +* [771d9401c](https://github.com/api-platform/core/commit/771d9401cffa0463928bb35ab52a56b269d57e1b) feat(elasticsearch): re-introduce v7 support (#6827) +* [b5372ddee](https://github.com/api-platform/core/commit/b5372ddeef2067333761104f3c17ed1b86001bf4) feat(openapi): filter x-apiplatform-tags to produce different openapi specifications (#6945) +* [2e2debb94](https://github.com/api-platform/core/commit/2e2debb94b06752262b0d18b736edc22b501266b) feat(mongodb): Replace usage of deprecated method `AggregationBuilder::execute()` (#6933) +* [4bdf042e5](https://github.com/api-platform/core/commit/4bdf042e5cab1ddb931562cf98a97686d8c415ae) feat(mongodb): Add pagination metadata to the aggregation results (#6912) +* [b968ccd50](https://github.com/api-platform/core/commit/b968ccd5026afb5562c4c4588929885b86ecb3cb) feat(openapi): document error outputs using json-schemas (#6923) +* [c97db6bb2](https://github.com/api-platform/core/commit/c97db6bb2f6b2db9a6a17141bdb56bd51e9fc50d) feat: swagger ui persist authorization option (#6877) +* [00787f32d](https://github.com/api-platform/core/commit/00787f32da54418de7d869cff218e22d8ae2ae1d) feat(laravel): automatically register policies (#6623) +* [12c42096b](https://github.com/api-platform/core/commit/12c42096bb0006d6ebae60ae5d90e9b356f9a335) feat(metadata): ability to hide an hydra class/operation (#6871) +* [57f15cf4f](https://github.com/api-platform/core/commit/57f15cf4f38278315c5f31d3949416c9455ba0d0) feat(state): strict query parameters (#6399) +* [bd0e92936](https://github.com/api-platform/core/commit/bd0e92936f82d3cd4563cd45ebf1f73fd1db9f01) feat(openapi): HTTP Authentication Support for Swagger UI (#6665) +* [be98f4e01](https://github.com/api-platform/core/commit/be98f4e01a52d8341ef9b65ed2f4e3b46ab31165) feat(graphql): allow to configure max query depth and max query complexity (#6880) +* [c78ed0b78](https://github.com/api-platform/core/commit/c78ed0b78baf5d2e1b7444a9882ba039c70a3887) feat(laravel): boolean filter (#6806) +* [d0a442786](https://github.com/api-platform/core/commit/d0a44278630d201b91cbba0774a09f4eeaac88f7) feat(doctrine): enhance getLinksHandler with method validation and typo suggestions (#6874) +* [f67f6f1ac](https://github.com/api-platform/core/commit/f67f6f1acb6476182c18a3503f2a8bc80ae89a0b) feat(doctrine): doctrine filters like laravel eloquent filters (#6775) + +## Notes + +This version as the 4.0.19 is compatible with Laravel 12. +The [hydra patch](#6748) changes default `hydra:title` and uses the resource `shortname`. Previously the `hydra:title` information was duplicating the `hydra:description`. +The `rdfs:label` got removed from the `hydra:Class` as it was used instead of the `hydra:title`. +On `hydra:property` `rdfs:label` got renamed to `label` as the `rdfs` namespace is available in the context. +The `ApiPlatform\Metadata\ErrorResource` and the `ConstraintViolation` (`ValidationException` class) are now generated directly from your PHP classes, only our `ConstraintViolationList` is hard-written and documents the `ConstraintViolation::violation` property. Therefore, your [own error resources](https://api-platform.com/docs/guides/error-resource/) are also documented. On top of that, we now set the `rdfs:subClassOf` to `hydra:Error`. +`#[ApiProperty(hydra: false)]` allows you to skip a documented `hydra:supportedProperty` on a class. +On write operations, we added the [expectsHeader](https://www.hydra-cg.com/spec/latest/core/#hydra:expectsHeader) field. + +## v4.1.0-beta.2 + +### Bug fixes + +* [da37ca91c](https://github.com/api-platform/core/commit/da37ca91c83e4f715c5acbbe3dc0296e8055bebc) fix(laravel): default to file cache instead of cache.default (#6955) +* [e041d721e](https://github.com/api-platform/core/commit/e041d721ea6fe24026eb021905740b25102d7966) fix: errors retrieval and documentation (#6952) +* [e6130cbcc](https://github.com/api-platform/core/commit/e6130cbccc1a8963b731769ef34f39e09f5d2cde) fix: missing filters on swagger ui entrypoint (#6950) + +### Features + +* [771d9401c](https://github.com/api-platform/core/commit/771d9401cffa0463928bb35ab52a56b269d57e1b) feat(elasticsearch): re-introduce v7 support (#6827) +* [b5372ddee](https://github.com/api-platform/core/commit/b5372ddeef2067333761104f3c17ed1b86001bf4) feat(openapi): filter x-apiplatform-tags to produce different openapi specifications (#6945) + +## v4.1.0-beta.1 + +### Features + +* [2e2debb94](https://github.com/api-platform/core/commit/2e2debb94b06752262b0d18b736edc22b501266b) feat(mongodb): Replace usage of deprecated method `AggregationBuilder::execute()` (#6933) +* [4bdf042e5](https://github.com/api-platform/core/commit/4bdf042e5cab1ddb931562cf98a97686d8c415ae) feat(mongodb): Add pagination metadata to the aggregation results (#6912) +* [b968ccd50](https://github.com/api-platform/core/commit/b968ccd5026afb5562c4c4588929885b86ecb3cb) feat(openapi): document error outputs using json-schemas (#6923) + +## v4.1.0-alpha.2 + +### Bug fixes + +* [01fd74268](https://github.com/api-platform/core/commit/01fd74268cf0e4a289b31ea74bea7f4e089b8361) fix(laravel): restore accidentally removed BooleanFilter +* [db40a63e7](https://github.com/api-platform/core/commit/db40a63e729fe03be84111dac2b0774fea4ab343) fix(hydra): rdfs:label should not duplicate title (#6748) +* [deb2ed265](https://github.com/api-platform/core/commit/deb2ed265dfee7b8a73fd3b542aef3e29eca3412) fix(laravel): fix use laravel fillable for writable props (#6898) + +### Features + +* [c97db6bb2](https://github.com/api-platform/core/commit/c97db6bb2f6b2db9a6a17141bdb56bd51e9fc50d) feat: swagger ui persist authorization option (#6877) + +### Notes + +The [hydra patch](#6748) changes default `hydra:title` and uses the resource `shortname`. Previously the `hydra:title` information was duplicating the `hydra:description`. +The `rdfs:label` got removed from the `hydra:Class` as it was used instead of the `hydra:title`. +On `hydra:property` `rdfs:label` got renamed to `label` as the `rdfs` namespace is available in the context. +The `ApiPlatform\Metadata\ErrorResource` and the `ConstraintViolation` (`ValidationException` class) are now generated directly from your PHP classes, only our `ConstraintViolationList` is hard-written and documents the `ConstraintViolation::violation` property. Therefore, your [own error resources](https://api-platform.com/docs/guides/error-resource/) are also documented. On top of that, we now set the `rdfs:subClassOf` to `hydra:Error`. +`#[ApiProperty(hydra: false)]` allows you to skip a documented `hydra:supportedProperty` on a class. +On write operations, we added the [expectsHeader](https://www.hydra-cg.com/spec/latest/core/#hydra:expectsHeader) field. + + +## v4.1.0-alpha.1 + +### Bug fixes + +* [67fbe51c5](https://github.com/api-platform/core/commit/67fbe51c570abe1ece6651ae6a037662e9012881) fix: reintroduce the `show_webby` parameter in Laravel config (#6741) + +### Features + +* [00787f32d](https://github.com/api-platform/core/commit/00787f32da54418de7d869cff218e22d8ae2ae1d) feat(laravel): automatically register policies (#6623) +* [12c42096b](https://github.com/api-platform/core/commit/12c42096bb0006d6ebae60ae5d90e9b356f9a335) feat(metadata): ability to hide an hydra class/operation (#6871) +* [57f15cf4f](https://github.com/api-platform/core/commit/57f15cf4f38278315c5f31d3949416c9455ba0d0) feat(state): strict query parameters (#6399) +* [bd0e92936](https://github.com/api-platform/core/commit/bd0e92936f82d3cd4563cd45ebf1f73fd1db9f01) feat(openapi): HTTP Authentication Support for Swagger UI (#6665) +* [be98f4e01](https://github.com/api-platform/core/commit/be98f4e01a52d8341ef9b65ed2f4e3b46ab31165) feat(graphql): allow to configure max query depth and max query complexity (#6880) +* [c78ed0b78](https://github.com/api-platform/core/commit/c78ed0b78baf5d2e1b7444a9882ba039c70a3887) feat(laravel): boolean filter (#6806) +* [d0a442786](https://github.com/api-platform/core/commit/d0a44278630d201b91cbba0774a09f4eeaac88f7) feat(doctrine): enhance getLinksHandler with method validation and typo suggestions (#6874) +* [f67f6f1ac](https://github.com/api-platform/core/commit/f67f6f1acb6476182c18a3503f2a8bc80ae89a0b) feat(doctrine): doctrine filters like laravel eloquent filters (#6775) + +## v4.0.22 + +### Bug fixes + +* [60747cc8c](https://github.com/api-platform/core/commit/60747cc8c2fb855798c923b5537888f8d0969568) fix(graphql): access to unauthorized resource using node Relay [CVE-2025-31481](https://github.com/api-platform/core/security/advisories/GHSA-cg3c-245w-728m) +* [7af65aad1](https://github.com/api-platform/core/commit/7af65aad13037d7649348ee3dcd88e084ef771f8) fix(graphql): property security might be cached w/ different objects [CVE-2025-31485](https://github.com/api-platform/core/security/advisories/GHSA-428q-q3vv-3fq3) +* [f4c426d71](https://github.com/api-platform/core/commit/f4c426d719b01debaa993b00d03cce8964057ecc) Revert "fix(doctrine): throw an exception when a filter is not found in a par…" (#7046) + +## v4.0.21 + +### Bug fixes + +* [7cb5a6db8](https://github.com/api-platform/core/commit/7cb5a6db87241d95e6c324318fe861bd4f1820cf) fix: allow parameter provider as object (#7032) +* [bb83e9a03](https://github.com/api-platform/core/commit/bb83e9a034c511156aa20a8555bf367374ef5458) fix(symfony): allow to merge array with string in global defaults (#7037) +* [da2e86809](https://github.com/api-platform/core/commit/da2e86809d4a8dec294dc2fc148d92406f1f7fd1) fix: header parameter should be case insensitive (#7031) + + +### Features + +## v4.0.20 + +### Bug fixes + +* [284937039](https://github.com/api-platform/core/commit/284937039c61d4516687c648f4a7581ec1686f3d) fix(doctrine): mapping ArrayAccess deprecation (#6982) +* [a434173b8](https://github.com/api-platform/core/commit/a434173b82f735041a79cb5f469ee0e731ca5956) fix(doctrine): Add a proper exception when a doctrine manager could not be found for a resource class (#6995) + + +### Features + +## v4.0.19 + +### Bug fixes + +* [1dfefda5e](https://github.com/api-platform/core/commit/1dfefda5e22b7ec03780e928e943097dd31ef68f) fix(laravel): add middleware granularity (#6962) + +Compatibility with Laravel 12. + +## v4.0.18 + +### Bug fixes + +* [7007dcca8](https://github.com/api-platform/core/commit/7007dcca8649814d871db7abe0fd65dc9c94a176) fix(laravel): duplicate middleware in routes +* [a16147ab7](https://github.com/api-platform/core/commit/a16147ab7b36f5238700c6ee9ee9253ce1424808) fix(laravel): handle route prefix (#6978) +* [d2e1963c5](https://github.com/api-platform/core/commit/d2e1963c56733b3174ae37cbc27a1374eba80bee) fix(symfony): detach objects to prevent loop when using Doctrine middleware and Mercure (#6936) (#6965) + + +### Features + +## v4.0.17 + +### Bug fixes + +* [5124d8c57](https://github.com/api-platform/core/commit/5124d8c571bc4324aa060e4ff808d48f0ffa8d73) fix(laravel): Prevent overwriting existing routes on the router (#6941) +* [5c7e0d2c0](https://github.com/api-platform/core/commit/5c7e0d2c015b5264b2f857abc7e6cb944582de21) fix(symfony): error wrongly inherit normalization context (#6939) +* [af35d34d0](https://github.com/api-platform/core/commit/af35d34d01e62e96dd81dadde6a056ce67d47703) fix(laravel): mitigate property metadata read for Error (#6951) +* [d5b48b1cd](https://github.com/api-platform/core/commit/d5b48b1cd6163ee755211476fdd3d4dd0bf6f7ae) fix(laravel): SwaggerUI custom CSS (#6937) +* [da796b979](https://github.com/api-platform/core/commit/da796b979384663c3eaf4e4fe4d215b447800844) fix(metadata): allow serializer attribute object in ApiProperty::$serialize (#6946) +* [de2d298e3](https://github.com/api-platform/core/commit/de2d298e306b196bce834af290aec242807ea39b) fix: ensure template files have a tpl file extension (#6826) (#6829) +* [b6a67a197](https://github.com/api-platform/core/commit/b6a67a197a668ce15f216cffbcddc637f19c69c2) perf: various optimizations for Laravel/Symfony (#6954) + +To save some time during cache warmup we recommend to define uri variables such as: `uriVariables: ['id']`. More details at #6954. + +### Features + +## v4.0.16 + +### Bug fixes + +* [dc4fc84ba](https://github.com/api-platform/core/commit/dc4fc84ba93e22b4f44a37e90a93c6d079c1c620) fix(graphql): securityAfterResolver not called + +### Features + +## v4.0.15 + +### Bug fixes + +* [36cee399c](https://github.com/api-platform/core/commit/36cee399cfd519355b03d0406921066a22ab474c) fix(state): skip Content-Location header for GET requests (#6901) +* [dba9de197](https://github.com/api-platform/core/commit/dba9de197001e91094e594f0e4dc638007cce7a6) fix(symfony): fix property restrictions for root resource with dynamic validation groups (#6908) + +### Features + +* [421d97ecf](https://github.com/api-platform/core/commit/421d97ecfdbc7d699a3d017d1e3ae3827a38b216) feat(laravel): add support for backed enum normalizers (#6911) + +Also contains [v3.4.15 changes](#v3415). + +## v4.0.14 + +### Bug fixes + +* [97cdb6b3f](https://github.com/api-platform/core/commit/97cdb6b3f43471789e096c9dc3a0c3c7b6d4e43c) fix(state): remove ProcessorInterface laravel specific type +* [b12a0d005](https://github.com/api-platform/core/commit/b12a0d005fda58a162b82a3574e6ee877838a55b) fix(graphql): register types for parameter args (#6895) + +Also contains [v3.4.14 changes](#v3414). + +## v4.0.13 + +### Bug fixes + +* [3a694624b](https://github.com/api-platform/core/commit/3a694624bdd6c82df756ff04682b5f90fd625f59) fix(laravel): eloquent BelongsToMany relation (#6862) +* [c8db7aef0](https://github.com/api-platform/core/commit/c8db7aef05557675940c3e610c94c6a2184d90ba) fix(laravel): jsonapi query parameters (page, sort, fields and include) (#6876) +* [f2c998158](https://github.com/api-platform/core/commit/f2c998158a70632a26efcdd29a17d7f3a2cb859c) fix(jsonld): anonymous context hydra_prefix value (#6873) + +Also contains [v3.4.10 changes](#v3410). + +## v4.0.12 + +### Bug fixes + +* [4db72f55f](https://github.com/api-platform/core/commit/4db72f55fa9dcd48518dc62b5bf472895b6a966b) fix: filter may not use FilterInterface (#6858) +* [c899a3da1](https://github.com/api-platform/core/commit/c899a3da14eb2dff49095d28855ef8f2a1c4072a) fix(laravel): use tagged resolvers as graphql resolvers + (#6855) +* [e0f8c38b9](https://github.com/api-platform/core/commit/e0f8c38b98a05f29ad36b37725e54a036209a859) fix(laravel): graphql currentPage (#6857) + +Also contains [v3.4.9 changes](#v349). + +## v4.0.11 + +### Bug fixes + +* [af66075fd](https://github.com/api-platform/core/commit/af66075fdd6b83bdebc1c4ca33cc0ab7e1a7f8af) fix(laravel): fix foregin keys (relations) beeing in attributes (#6843) + +* [2d59c6369](https://github.com/api-platform/core/commit/2d59c63699b4602cfe4d62504896c6d4121c1be4) feat(laravel): belongs to many relations (#6818) + +Also contains [v3.4.8 changes](#v348). + +## v4.0.10 + +### Bug fixes + +* [774590069](https://github.com/api-platform/core/commit/77459006912d9162fdae9eadd496a62858ac990e) fix(laravel): add contact & license options to fix swagger UI issues (#6804) +* [7ff9790c8](https://github.com/api-platform/core/commit/7ff9790c8e326de8152c653974fd523770dc6501) fix(laravel): allow boolean cast (#6808) +* [f19dd9446](https://github.com/api-platform/core/commit/f19dd9446f5722d243e82b1e7bc34ebdab95719d) fix(laravel): graphQl type locator indexes (#6815) + +Also contains [v3.4.7 changes](#v347). + +## v4.0.9 + +### Bug fixes + +* [417fef5da](https://github.com/api-platform/core/commit/417fef5da9b1f3f16e323a193dec141f13b1ebc5) fix(laravel): overlaping route format (#6782) +* [81099065e](https://github.com/api-platform/core/commit/81099065e30eb0356881907bd52095b85b9cae3d) fix(laravel): declare normalizer list as a service (#6786) +* [dc8c09b1e](https://github.com/api-platform/core/commit/dc8c09b1e1ac15a7bcd4961fc3e80b06bec82e77) fix(laravel) graphQl Relationship loading (#6792) + +Also contains [v3.4.6 changes](#v346). + +## v4.0.8 + +### Bug fixes + +* [dddb97075](https://github.com/api-platform/core/commit/dddb97075af9c6e2517e1881b803c9d31a1913cf) fix(symfony): default formats order (#6780) + +## v4.0.7 + +### Bug fixes + +* [17c916c3a](https://github.com/api-platform/core/commit/17c916c3a1bcc837c9bc842dc48390dbeb043450) fix(symfony): BackedEnumProvider typo fix (#6769) +* [216d9ccaa](https://github.com/api-platform/core/commit/216d9ccaacf7845daaaeab30f3a58bb5567430fe) fix(serializer): fetch type on normalization error when possible (#6761) +* [2f967d934](https://github.com/api-platform/core/commit/2f967d9345004779f409b9ce1b5d0cbba84c7132) fix(doctrine): throw an exception when a filter is not found in a parameter (#6767) +* [6c9b508b0](https://github.com/api-platform/core/commit/6c9b508b030976741a68f84e226502e6c218e896) fix(laravel): remove link header when jsonld is not enabled (#6768) +* [736ca045e](https://github.com/api-platform/core/commit/736ca045e6832f04aaa002ddd7b85c55df4696bb) fix(validator): allow to pass both a ConstraintViolationList and a previous exception (#6762) +* [9ac3661b6](https://github.com/api-platform/core/commit/9ac3661b6a75255832203b87a9ba7994add64061) fix(hydra): store and use hydra context in a local variable (#6765) + +## v4.0.6 + +### Bug fixes + +* [195c4e788](https://github.com/api-platform/core/commit/195c4e7883520416e042ac78143b18652a216fbf) fix(hydra): hydra context changed (#6710) +* [4f65ef2d0](https://github.com/api-platform/core/commit/4f65ef2d061215df348e3505856f0f41c7c909ed) fix(metadata): providing parameter constraints skips automatic ones (#6756) +* [5a8ef115a](https://github.com/api-platform/core/commit/5a8ef115a90791992a6c1325fb6d1ac458b22153) fix(symfony): ECMA-262 pattern with RegExp validator (#6733) +* [67c5a2a24](https://github.com/api-platform/core/commit/67c5a2a2463bca94f0997b4fab1248a08994465b) fix(laravel): jsonapi error serialization (#6755) +* [ac6f667f3](https://github.com/api-platform/core/commit/ac6f667f301f6c4c399a707faf00567239bd98d8) fix(laravel): collection relations other than HasMany (#6737) + +* [cecd77149](https://github.com/api-platform/core/commit/cecd77149795c1a455ac72bc3ed0606413e69900) feat(laravel): use laravel cache setting (#6751) + +## v4.0.5 + +### Bug fixes + +* [4171d5f9c](https://github.com/api-platform/core/commit/4171d5f9cd41731b857c53a186270ba0626baedf) fix(graphql): register query parameter arguments with filters (#6726) +* [48ab53816](https://github.com/api-platform/core/commit/48ab53816c55e6116aa64ac81f522f4b7b9bb9f6) fix(laravel): make command writes to app instead of src (#6723) + +## v4.0.4 + +### Bug fixes + +* [2e8287dad](https://github.com/api-platform/core/commit/2e8287dad0c0315dd6527279a6359c0a22f40d93) fix(laravel): allow serializer attributes through ApiProperty (#6680) +* [439c188ea](https://github.com/api-platform/core/commit/439c188ea1685676d5e705a49a4b835f35a84d72) fix(laravel): match integer type (#6715) +* [4ad7a50aa](https://github.com/api-platform/core/commit/4ad7a50aaabf0d85e2eb5bb3a6d4ef8d5b7b39a7) fix(laravel): openapi Options binding (#6714) +* [ec6e64512](https://github.com/api-platform/core/commit/ec6e6451299a50fcab397e86fafe6db132ce7519) fix(laravel): skip resource path when not available (#6697) + +### Features + +* [5aa799321](https://github.com/api-platform/core/commit/5aa7993219a6fb55f11476a031963a542b2d3586) feat(laravel): command to generate state providers/processors (#6708) + +## v4.0.3 + +### Bug fixes + +* [025f63e69](https://github.com/api-platform/core/commit/025f63e69c2ec655a828559ed78c49a365ca043b) fix(laravel): route registration of EntrypointController should be last (#6667) +* [2b4937a3e](https://github.com/api-platform/core/commit/2b4937a3e09fb891b99fd8499b597190a4b740e0) fix(laravel): eloquent accessors (#6668) +* [4312a1f55](https://github.com/api-platform/core/commit/4312a1f55f4f80152be93734cb5cf73c70dee53a) fix(metadata): register parameters on graphql operations +* [6d4e24883](https://github.com/api-platform/core/commit/6d4e24883767f1c58dff5e52f57b0422110fa38f) fix(laravel): hiding/showing relationships (#6679) +* [85306f2f5](https://github.com/api-platform/core/commit/85306f2f5a7d480b1570471689d1d3ca4e9846a3) fix(laravel): swagger ui authentication (#6661) +* [a6e37068e](https://github.com/api-platform/core/commit/a6e37068ea49d1b5a4ee098a62a287d62fba1c35) fix(laravel): use Model::qualifyColumn instead of hardcoding $table.$column (#6658) +* [b0d5a2ade](https://github.com/api-platform/core/commit/b0d5a2adedb583074aa93d4f641bdda419d31ffa) fix(laravel): register global middleware to secure non-rest routes +* [f9d96e546](https://github.com/api-platform/core/commit/f9d96e546a37121244ab98d65c2d91f48b1bb112) fix(metadata): graphql can be disabled but with an existing operation + + +### Features + +* [df701da05](https://github.com/api-platform/core/commit/df701da05620a847f529ebabaee97f8cf5ecb37f) feat(laravel): graphql policies + +## v4.0.2 + +### Bug fixes + +* [219199db3](https://github.com/api-platform/core/commit/219199db386cab05f1c1225b889c0a9609b36699) fix(symfony): missing alias to serializer context builder interface (#6643) +* [5f943e3bb](https://github.com/api-platform/core/commit/5f943e3bb56934ba5d0b858f6b4c20a2985b6b6b) fix(graphql): wrong exception namespace (#6647) +* [72a0b669a](https://github.com/api-platform/core/commit/72a0b669a426ca4bbbf14cf80a6ced683b947e8c) fix(serializer): remove serializer context builder interface +* [88bd8c3e1](https://github.com/api-platform/core/commit/88bd8c3e151c843649bcac3feefc2cb956212410) fix(laravel): installation command, fix config overwrites (#6649) +* [93314b08d](https://github.com/api-platform/core/commit/93314b08de1e6f0505af9e3a3ba3d9971f1ef09c) fix(serializer): allow state's SerializerFilterContextBuilderInterface (#6632) +* [9a0afc917](https://github.com/api-platform/core/commit/9a0afc917a4bfa824ffbb640af9bb1114a5d31b4) fix(serializer): remove unnecessary dependency +* [c47e2996e](https://github.com/api-platform/core/commit/c47e2996e51c587c998fde88903703bd6ac9a43c) fix: default format and standard_put values +* [e327f5f69](https://github.com/api-platform/core/commit/e327f5f69c823c1ed674eefc0eb2551e30fb36bd) fix(symfony): namespace of path segment name generator services (#6642) + +Notes: + +`standard_put=true` is now the default, you can set it to `false` using `extra_properties.defaults` + +## v4.0.1 + +### Bug fixes + +* [eb80a1a56](https://github.com/api-platform/core/commit/eb80a1a5651b81cab13b018662a0d21e05facbfe) fix(state): precise format on content-location (#6627) + +### Features + +* [4a2271670](https://github.com/api-platform/core/commit/4a2271670a88c318ab38bd7eb2a1c0b93a5c0ea0) feat: api-platform/json-hal component (#6621) + +## v4.0.0 + +### Bug fixes + +* [7c5689626](https://github.com/api-platform/core/commit/7c568962634691892fe1057b3c982765a1c20ba2) fix(laravel): call authorize on delete but not validation (#6618) +* [d74b2b5fa](https://github.com/api-platform/core/commit/d74b2b5fa939a9f5ca11d3538e358070047a6c3d) fix: swagger ui with route identifier (#6616) +* [de6e3f546](https://github.com/api-platform/core/commit/de6e3f546a26c5ad5444d8e2448d81faec36bd73) fix(laravel): validate enum schema within filter (#6615) +* [0a461d749](https://github.com/api-platform/core/commit/0a461d749b7b4ac706f3b7b6138a13cb6e4a9d2d) fix(symfony): allow schema restriction for collection like property from choice constraint (#6520) +* [0c66a494d](https://github.com/api-platform/core/commit/0c66a494d3bda5817e59da2e43f0232e2e8fea15) fix(laravel): cache metadata, add trace on debug mode (#6555) +* [1b6e7c6cc](https://github.com/api-platform/core/commit/1b6e7c6ccf3d2c61816a7dceb35f1d5980ea0565) fix(laravel): disable GraphQL by default and fix provider +* [290944103](https://github.com/api-platform/core/commit/290944103039a4a6d64904d1a89264b800c809d5) fix(laravel): SwaggerUI title (#6527) +* [2df0860b5](https://github.com/api-platform/core/commit/2df0860b577bb1ae0882096436f3eaeb91281901) fix(laravel): Eloquent date and datetime type detection (#6529) +* [2fc74f2e6](https://github.com/api-platform/core/commit/2fc74f2e651f229e982343c1cb0c6a2c5d5eee64) fix: remove PUT from default operations (#6570) +* [3b42c9ff2](https://github.com/api-platform/core/commit/3b42c9ff235de5feac555d0283c513a6e4643953) fix: deserialization path for not denormalizable relations collected errors (#6537) +* [3c554a605](https://github.com/api-platform/core/commit/3c554a605ec9d5f36dfd852c4f93f0ce582064c9) fix(laravel): docs _format and open swagger ui (#6595) +* [3c5aea80f](https://github.com/api-platform/core/commit/3c5aea80fdbed20216764f6d721fe4f37cf2889d) fix(symfony): load isApiResource metadata (#6562) +* [4ee209eff](https://github.com/api-platform/core/commit/4ee209effb0fd11789db9f016d6e90aa3cb942a9) fix(laravel): visible and hidden fields support (#6538) +* [6e15eb95f](https://github.com/api-platform/core/commit/6e15eb95fffb2deec3d381f5d6fd87e189772270) fix(laravel): register HydraPartialCollectionViewNormalizer (#6588) +* [86365be2a](https://github.com/api-platform/core/commit/86365be2a5b8e8d0050e09d4e401bb758aa8b7a8) fix(laravel): Eloquent PropertyAccessor (#6536) +* [a1dd0b54d](https://github.com/api-platform/core/commit/a1dd0b54d137d70f2163fa03690c1f4c74a549c0) fix(laravel): entrypoint with doc formats (#6552) +* [a4a53ab48](https://github.com/api-platform/core/commit/a4a53ab4838f4d17d3677952157b44ec165e3e3a) fix(laravel): identitifer is not writable unless marked as writable (#6531) +* [a6f355358](https://github.com/api-platform/core/commit/a6f355358a88e7cf7759db0dee41e185157ddc68) fix(laravel): do not normalize exception originalTrace (#6533) +* [bd6a57c4c](https://github.com/api-platform/core/commit/bd6a57c4c0b38d6f880a5bd79031a87033f707e6) fix(laravel): snake case props (#6532) +* [c31566602](https://github.com/api-platform/core/commit/c315666022185839742b8c5ef81601d85d8c3f4b) fix(laravel): api_doc route regex +* [ebc61d59d](https://github.com/api-platform/core/commit/ebc61d59d60eda3a020593cf4cb46c5d30548e46) fix(laravel): entrypoint serialization (#6541) + +### Features + +* [00787f32d](https://github.com/api-platform/core/commit/00787f32da54418de7d869cff218e22d8ae2ae1d) feat(laravel): automatically register policies (#6623) +* [06a647a80](https://github.com/api-platform/core/commit/06a647a80d4c6b7bfb3474d0685bcb445b56a5a8) feat(laravel): add CSV support (#6617) +* [a49bde1ea](https://github.com/api-platform/core/commit/a49bde1ea79ae4226b70c20f9bf967ac77e9ab89) feat(laravel): filter validations rules +* [03357fb90](https://github.com/api-platform/core/commit/03357fb90ac0003f0cec2002df01711d0fb99a1e) feat(laravel): supports more Eloquent types (#6544) +* [05e75be83](https://github.com/api-platform/core/commit/05e75be834c629e0487caaaedfe9fdf0bd5a7226) feat(doctrine): add new filter for filtering an entity using PHP backed enum, resolves #6506 (#6547) (#6560) +* [538648840](https://github.com/api-platform/core/commit/538648840dc7439b12033ed6f413f3167705da4d) feat(laravel): enable graphQl support (#6550) +* [5b9767be2](https://github.com/api-platform/core/commit/5b9767be215afdf61fccf8490d0a3a7018078ce5) feat(laravel): policy, auth and gate (#6523) +* [5e1233c57](https://github.com/api-platform/core/commit/5e1233c57a497d00a7c4bd3e3ad0cac25aeac014) feat(laravel): search filter (#6534) +* [9c461626f](https://github.com/api-platform/core/commit/9c461626f7d02f8d15487134b636f11966c19a5e) feat(laravel): provide a trait in addition to the annotation (#6543) +* [c9f18d4fb](https://github.com/api-platform/core/commit/c9f18d4fb833ea0b89ef18021cad491cf0600ef1) feat(laravel): eloquent filters (search, date, equals, or) (#6593) +* [e09e73efc](https://github.com/api-platform/core/commit/e09e73efc5b4a39ab33d00c5d5422d8d9f7b5e89) feat: remove hydra prefix (#6418) + +## v4.0.0-alpha.5 + +### Bug fixes + +* [0a461d749](https://github.com/api-platform/core/commit/0a461d749b7b4ac706f3b7b6138a13cb6e4a9d2d) fix(symfony): allow schema restriction for collection like property from choice constraint (#6520) +* [0c66a494d](https://github.com/api-platform/core/commit/0c66a494d3bda5817e59da2e43f0232e2e8fea15) fix(laravel): cache metadata, add trace on debug mode (#6555) +* [1b6e7c6cc](https://github.com/api-platform/core/commit/1b6e7c6ccf3d2c61816a7dceb35f1d5980ea0565) fix(laravel): disable GraphQL by default and fix provider +* [290944103](https://github.com/api-platform/core/commit/290944103039a4a6d64904d1a89264b800c809d5) fix(laravel): SwaggerUI title (#6527) +* [2df0860b5](https://github.com/api-platform/core/commit/2df0860b577bb1ae0882096436f3eaeb91281901) fix(laravel): Eloquent date and datetime type detection (#6529) +* [2fc74f2e6](https://github.com/api-platform/core/commit/2fc74f2e651f229e982343c1cb0c6a2c5d5eee64) fix: remove PUT from default operations (#6570) +* [3b42c9ff2](https://github.com/api-platform/core/commit/3b42c9ff235de5feac555d0283c513a6e4643953) fix: deserialization path for not denormalizable relations collected errors (#6537) +* [3c554a605](https://github.com/api-platform/core/commit/3c554a605ec9d5f36dfd852c4f93f0ce582064c9) fix(laravel): docs _format and open swagger ui (#6595) +* [3c5aea80f](https://github.com/api-platform/core/commit/3c5aea80fdbed20216764f6d721fe4f37cf2889d) fix(symfony): load isApiResource metadata (#6562) +* [4ee209eff](https://github.com/api-platform/core/commit/4ee209effb0fd11789db9f016d6e90aa3cb942a9) fix(laravel): visible and hidden fields support (#6538) +* [6e15eb95f](https://github.com/api-platform/core/commit/6e15eb95fffb2deec3d381f5d6fd87e189772270) fix(laravel): register HydraPartialCollectionViewNormalizer (#6588) +* [86365be2a](https://github.com/api-platform/core/commit/86365be2a5b8e8d0050e09d4e401bb758aa8b7a8) fix(laravel): Eloquent PropertyAccessor (#6536) +* [a1dd0b54d](https://github.com/api-platform/core/commit/a1dd0b54d137d70f2163fa03690c1f4c74a549c0) fix(laravel): entrypoint with doc formats (#6552) +* [a4a53ab48](https://github.com/api-platform/core/commit/a4a53ab4838f4d17d3677952157b44ec165e3e3a) fix(laravel): identitifer is not writable unless marked as writable (#6531) +* [a6f355358](https://github.com/api-platform/core/commit/a6f355358a88e7cf7759db0dee41e185157ddc68) fix(laravel): do not normalize exception originalTrace (#6533) +* [bd6a57c4c](https://github.com/api-platform/core/commit/bd6a57c4c0b38d6f880a5bd79031a87033f707e6) fix(laravel): snake case props (#6532) +* [c31566602](https://github.com/api-platform/core/commit/c315666022185839742b8c5ef81601d85d8c3f4b) fix(laravel): api_doc route regex +* [ebc61d59d](https://github.com/api-platform/core/commit/ebc61d59d60eda3a020593cf4cb46c5d30548e46) fix(laravel): entrypoint serialization (#6541) + + +### Features + +* [03357fb90](https://github.com/api-platform/core/commit/03357fb90ac0003f0cec2002df01711d0fb99a1e) feat(laravel): supports more Eloquent types (#6544) +* [05e75be83](https://github.com/api-platform/core/commit/05e75be834c629e0487caaaedfe9fdf0bd5a7226) feat(doctrine): add new filter for filtering an entity using PHP backed enum, resolves #6506 (#6547) (#6560) +* [538648840](https://github.com/api-platform/core/commit/538648840dc7439b12033ed6f413f3167705da4d) feat(laravel): enable graphQl support (#6550) +* [5b9767be2](https://github.com/api-platform/core/commit/5b9767be215afdf61fccf8490d0a3a7018078ce5) feat(laravel): policy, auth and gate (#6523) +* [5e1233c57](https://github.com/api-platform/core/commit/5e1233c57a497d00a7c4bd3e3ad0cac25aeac014) feat(laravel): search filter (#6534) +* [9c461626f](https://github.com/api-platform/core/commit/9c461626f7d02f8d15487134b636f11966c19a5e) feat(laravel): provide a trait in addition to the annotation (#6543) +* [c9f18d4fb](https://github.com/api-platform/core/commit/c9f18d4fb833ea0b89ef18021cad491cf0600ef1) feat(laravel): eloquent filters (search, date, equals, or) (#6593) +* [e09e73efc](https://github.com/api-platform/core/commit/e09e73efc5b4a39ab33d00c5d5422d8d9f7b5e89) feat: remove hydra prefix (#6418) + +## v4.0.0-alpha.1 + +### Bug fixes + +* [f7f9f5427](https://github.com/api-platform/core/commit/f7f9f542719ad43d6e0fe74024a2fa05c11cb6fa) fix(laravel): phpstan/doc-parser is mandatory + +### Features + +* [0d5f35683](https://github.com/api-platform/core/commit/0d5f356839eb6aa9f536044abe4affa736553e76) feat(laravel): laravel component (#5882) + +## v3.4.16 + +### Bug fixes + +* [dc4fc84ba](https://github.com/api-platform/core/commit/dc4fc84ba93e22b4f44a37e90a93c6d079c1c620) fix(graphql): securityAfterResolver not called +* [9eb5c4e94](https://github.com/api-platform/core/commit/9eb5c4e941d0ebf59bc8ef5777b144db9b4a0899) fix(symfony): suggest `DocumentationAction` as replacement for deprecated `SwaggerUiAction` (#6894) + +## v3.4.15 + +### Bug fixes + +* [ab03b5544](https://github.com/api-platform/core/commit/ab03b5544f742b98a39cc23fc157f1be7a2e0c63) fix(openapi): typing issue with `openapiContext` in `#[ApiProperty]` (#6910) + +## v3.4.14 + +### Bug fixes + +* [0cf752bce](https://github.com/api-platform/core/commit/0cf752bcec692718b2503250e655d05aea670316) fix(metadata): make the schema attribute to fallback to null for parameters in YamlResourceExtractor (#6896) +* [2b3c55db2](https://github.com/api-platform/core/commit/2b3c55db2a9ecc52f62c441fa8a5696233a30b87) fix(symfony): remove unsolvable deprecation (#6899) see also (#6655) +* [9493b9b6e](https://github.com/api-platform/core/commit/9493b9b6ec0264ab5b700c861ad1b97455b4f88d) fix(symfony): revert json schema bc break (#6903) +* [b82f9ac76](https://github.com/api-platform/core/commit/b82f9ac76ce89dd3910849c73da42317ee1339ed) fix(openapi): not forbidden response on openAPI doc (#6886) + +I mispublished a v3.4.13 on some repositories to fix them all I bumped 3.4.10 to 3.4.14 +More details at #6888. + +## v3.4.10 + +### Bug fixes + +* [2ee5eb496](https://github.com/api-platform/core/commit/2ee5eb4967f507d04ae07280914bea3c712d8cad) fix(symfony): mercure exception formatting by calling array_keys() (#6879) + +## v3.4.9 + +### Bug fixes + +* [22cbd0147](https://github.com/api-platform/core/commit/22cbd0147ef6f817093533d62dc8279add67a647) fix(metadata): various parameter improvements (#6867) +* [978975ef0](https://github.com/api-platform/core/commit/978975ef01d7b9d230291676527aa1140a7e552f) fix(jsonschema): hashmaps produces invalid openapi schema (#6830) +* [a209dd440](https://github.com/api-platform/core/commit/a209dd440957176099247acf35b82611073352b1) fix: add missing error normalizer trait and remove deprecated interface (#6853) +* [abbc031ee](https://github.com/api-platform/core/commit/abbc031eece83b54781502cd6373b47a09e109f4) fix: test empty parameter against null (#6852) + +### Notes + +- `Parameter::getValue()` now takes a default value as argument `getValue(mixed $default = new ParameterNotFound()): mixed` +- `Parametes::get(string $key, string $parameterClass = QueryParameter::class)` (but also `has` and `remove`) now has a default value as second argument to `QueryParameter::class` +- Constraint violation had the wrong message when using `property`, fixed by using the `key` instead + +## v3.4.8 + +### Bug fixes + +* [4d7deeaf7](https://github.com/api-platform/core/commit/4d7deeaf794178b5496ae989520095831a86df8a) fix(jsonld): check if supportedTypes exists (#6825) +* [5111935d4](https://github.com/api-platform/core/commit/5111935d4f917920c6f3d24b828f9d59fd0e3520) fix(symfony): object typed property schema collection restriction (#6823) +* [6bf894f6f](https://github.com/api-platform/core/commit/6bf894f6f0ead0751936aeddcfc527f017498bb3) fix(serializer): use attribute denormalization context for constructor arguments (#6821) +* [86c97cac3](https://github.com/api-platform/core/commit/86c97cac3b8d45b6190e2999b99a02e88dd4e527) fix(symfony): symfony 7.2 deprecations (#6835) +* [d312eae7e](https://github.com/api-platform/core/commit/d312eae7ef590ec05139c09bfaf2d3c7668a3f22) fix(doctrine): fixed orm datefilter applying inner join when no filtering values have been provided (#6849) + +## v3.4.7 + +### Bug fixes + +* [2d25e79e0](https://github.com/api-platform/core/commit/2d25e79e0e04ce549fb67ecc2017798a8deb7458) fix(symfony): unset item_uri_template when serializing an error (#6816) + +## v3.4.6 + +### Bug fixes + +* [17c916c3a](https://github.com/api-platform/core/commit/17c916c3a1bcc837c9bc842dc48390dbeb043450) fix(symfony): service typo fix BackedEnumProvider for autowiring (#6769) +* [216d9ccaa](https://github.com/api-platform/core/commit/216d9ccaacf7845daaaeab30f3a58bb5567430fe) fix(serializer): fetch type on normalization error when possible (#6761) +* [2f967d934](https://github.com/api-platform/core/commit/2f967d9345004779f409b9ce1b5d0cbba84c7132) fix(doctrine): throw an exception when a filter is not found in a parameter (#6767) +* [736ca045e](https://github.com/api-platform/core/commit/736ca045e6832f04aaa002ddd7b85c55df4696bb) fix(validator): allow to pass both a ConstraintViolationList and a previous exception (#6762) +* [a98332d99](https://github.com/api-platform/core/commit/a98332d99a43338fa3bc0fd6b20f82ac58d1c397) fix(metadata): name convert parameter property (#6766) +* [aa1667de1](https://github.com/api-platform/core/commit/aa1667de116fa9a40842f1480fc90ab49c7c2784) fix(state): empty result when the array paginator is out of bound (#6785) +* [ab88353a3](https://github.com/api-platform/core/commit/ab88353a32f94146b01c34bae377ec5a735846db) fix(hal): detecting and handling circular reference (#6752) +* [bba030614](https://github.com/api-platform/core/commit/bba030614b96887fea4f5c177e3137378ccae8a5) fix: properly support phpstan/phpdoc-parser 2 (#6789) +* [bec147b91](https://github.com/api-platform/core/commit/bec147b916c29e346a698b28ddd4493bf305d9a0) fix(state): do not check content type if no input (#6794) + +## v3.4.5 + +### Bug fixes + +* [fc8fa00a1](https://github.com/api-platform/core/commit/fc8fa00a19320b65547a60537261959c11f8e6a8) fix(hydra): iri template when using query parameter (#6742) + +## v3.4.4 + +### Bug fixes + +* [550347867](https://github.com/api-platform/core/commit/550347867f30611b673d8df99f65186d013919dd) fix(graphql): register query parameter arguments with filters (#6727) +* [99262dce7](https://github.com/api-platform/core/commit/99262dce739800bd841c95e026848b587ba25801) fix(jsonschema): handle @id when genId is false (#6716) +* [ad5efa535](https://github.com/api-platform/core/commit/ad5efa535a4dcbaad64ecff89514eaa6e07f5b7c) fix: multiple parameter provider #6673 (#6732) +* [d34cd7be8](https://github.com/api-platform/core/commit/d34cd7be8e7a12fd08a8b10270a614c06c10aa89) fix: use stateOptions when retrieving a Parameter filter (#6728) +* [e7fb04fab](https://github.com/api-platform/core/commit/e7fb04fab05bc077e2dbeb0fa0fc2c1d28c96105) fix(symfony): fetch api-platform/symfony version debug bar (#6722) +* [e96623ebf](https://github.com/api-platform/core/commit/e96623ebfd8691ba943bdb56a4d91e160497a311) fix(jsonld): prefix error @type with hydra: (#6721) + +## v3.4.3 + +### Bug fixes + +* [3ca599158](https://github.com/api-platform/core/commit/3ca599158139d56fbd6ee66f2de3e586120d735c) fix(hydra): hydra_prefix on errors (#6704) +* [f7f605dc8](https://github.com/api-platform/core/commit/f7f605dc8b798b975d2286c970c9091436d7f890) fix: check that api-platform/ramsey-uuid is installed before registering related services (#6696) +* [fbb53e5e3](https://github.com/api-platform/core/commit/fbb53e5e35ca0ec3de26ddc7de7ea4d1dda5c20b) fix(symfony): metadata aware name converter has 0 arguments by default (#6711) + +## v3.4.2 + +### Bug fixes + +* [0ca76fc89](https://github.com/api-platform/core/commit/0ca76fc898d2d1a679a490a5dea85473bf680901) fix(elasticsearch): allow elasticsearch 7 (#6689) +* [15d61c4b7](https://github.com/api-platform/core/commit/15d61c4b75fea2b365e0852a923fed8efbae6ab8) fix(metadata): using parameters in fromClass and toClass uriVariables' options (#6663) +* [2e2044636](https://github.com/api-platform/core/commit/2e204463675939903128037f82916d68f0016719) fix(metadata): parameter provider in a long running http worker (#6683) +* [4c58b33e8](https://github.com/api-platform/core/commit/4c58b33e8c5a90f9377543bd068288dcf84e3236) fix(jsonapi): fixed definition name to allow using the same class names in different namespaces (#6676) +* [4f5f56756](https://github.com/api-platform/core/commit/4f5f5675629fe52ea415a6bd91f3625eedea9c87) fix: remove hydra prefix on errors (#6624) +* [afe7d47d7](https://github.com/api-platform/core/commit/afe7d47d7b7ba6c8591bfb60137a65d1fa1fe38f) fix(metadata): passing class as parameter in XML ApiResource's definition (#6659) +* [b93ee467c](https://github.com/api-platform/core/commit/b93ee467c69253e0cfe60e75b48a5c7aa683474a) fix(metadata): overwriting XML ApiResource definition by YAML ApiResource definition (#6660) + +## v3.4.1 + +### Bug fixes + +* [219199db3](https://github.com/api-platform/core/commit/219199db386cab05f1c1225b889c0a9609b36699) fix(symfony): missing alias to serializer context builder interface (#6643) +* [5f943e3bb](https://github.com/api-platform/core/commit/5f943e3bb56934ba5d0b858f6b4c20a2985b6b6b) fix(graphql): wrong exception namespace (#6647) +* [93314b08d](https://github.com/api-platform/core/commit/93314b08de1e6f0505af9e3a3ba3d9971f1ef09c) fix(serializer): allow state's SerializerFilterContextBuilderInterface (#6632) +* [9a0afc917](https://github.com/api-platform/core/commit/9a0afc917a4bfa824ffbb640af9bb1114a5d31b4) fix(serializer): remove unnecessary dependency +* [e327f5f69](https://github.com/api-platform/core/commit/e327f5f69c823c1ed674eefc0eb2551e30fb36bd) fix(symfony): namespace of path segment name generator services (#6642) + +## v3.4.0 + +### Deprecations: + +Namespaces like `ApiPlatform/Api` or `ApiPlatform/Util` are deprecated and will be removed in 4.0. +You should now install `api-platform/symfony` instead of `api-platform/core`. + +Read the [upgrade guide](https://api-platform.com/docs/core/upgrade-guide/) for detailed steps. + +### Bug fixes + +* [56153b755](https://github.com/api-platform/core/commit/56153b755151ee11c8c17fdc3fd919d544f078ac) fix(hydra): error hydra prefix (#6599) +* [7c89f66a5](https://github.com/api-platform/core/commit/7c89f66a534cc1100c3dab0a129381d307d9d8b4) fix: replace ApiPlatform\\Exception use by ApiPlatform\Metadata\\Exception (#6597) +* [a3a4a990d](https://github.com/api-platform/core/commit/a3a4a990d527136f093b022782a82e1d5b04c0b5) fix(metadata): loop on operations can be null +* [ef0ee6427](https://github.com/api-platform/core/commit/ef0ee6427f8056bcb2617c228a7cf9ffd9d29ccd) fix(doctrine): use parameter.property for filter value (#6572) +* [17c6b586c](https://github.com/api-platform/core/commit/17c6b586c5ab49437ac11dd092efdd5f0baf569b) fix(state): log on missing provider (#6519) +* [601ccfb42](https://github.com/api-platform/core/commit/601ccfb4243803f40a7fa7179e0661da59c88b86) fix(doctrine): move event listeners to doctrine/common (#6573) +* [6499e0aa5](https://github.com/api-platform/core/commit/6499e0aa5dd61fdff7706e7940cdf8c1fc3e18ef) fix: deprecate url generator interface namespace (#6575) +* [3c5aea80f](https://github.com/api-platform/core/commit/3c5aea80fdbed20216764f6d721fe4f37cf2889d) fix(symfony): load isApiResource metadata (#6562) +* [61af0cc90](https://github.com/api-platform/core/commit/61af0cc90c1e095edb12e32ef433a742ef46637e) fix(doctrine): allow doctrine/dbal:^4 +* [e063b80af](https://github.com/api-platform/core/commit/e063b80afe012ca4a6c8999de55b59193e8ae0ae) fix: parameter context for filters (#6535) +* [e22392193](https://github.com/api-platform/core/commit/e22392193bb1fc71ece5abf393fa54b0745fc287) fix(state): security parameter with listeners (#6457) + +Various fixes for components isolation. + +### Features + +* [130fb5a8c](https://github.com/api-platform/core/commit/130fb5a8c833430e5e09624b06f296e0bcb7ceed) feat: better path sorting for openapi UIs (#6583) +* [26d700e06](https://github.com/api-platform/core/commit/26d700e06035eaf4d04ddd52f3101dae690734d8) feat(symfony): add error page (#6389) +* [48267c9b6](https://github.com/api-platform/core/commit/48267c9b6a942b1fb54f0efcfa5b2d2ac47c93bf) feat(openapi): add error resources schemes (#6332) +* [e91c783a2](https://github.com/api-platform/core/commit/e91c783a2abf51bf2bdcf1230826108632c44a0d) feat(state): "deserializer_type" context (#6429) +* [17c6b586c](https://github.com/api-platform/core/commit/17c6b586c5ab49437ac11dd092efdd5f0baf569b) feat(state): log on missing provider (#6519) +* [05e75be83](https://github.com/api-platform/core/commit/05e75be834c629e0487caaaedfe9fdf0bd5a7226) feat(doctrine): add new filter for filtering an entity using PHP backed enum, resolves #6506 (#6547) (#6560) +* [0b985ae76](https://github.com/api-platform/core/commit/0b985ae760bc4689d3f5bbacebb21b35b334d0be) feat(state): add security to parameters (#6435) +* [63ccfd58c](https://github.com/api-platform/core/commit/63ccfd58c95b5aa4aa0353eb122d96ef35187222) feat: BackedEnum resources (#6309) +* [65296eaf1](https://github.com/api-platform/core/commit/65296eaf1eb18dc725e9316e9ab49b191aae43a3) feat(openapi): allow optional request body content (#6374) +* [7399fcf7e](https://github.com/api-platform/core/commit/7399fcf7eaf28cd649d137da1fdd54f69093e275) feat(symfony): skip error handler (#6463) +* [74986cb55](https://github.com/api-platform/core/commit/74986cb552182dc645bd1fc967faa0954dd59e0a) feat: inflector as service (#6447) +* [b47edb2a4](https://github.com/api-platform/core/commit/b47edb2a499c34e79c167f963e3a626a3e9d040a) feat(serializer): context IRI in HAL or JsonApi format (#6215) +* [db1241c66](https://github.com/api-platform/core/commit/db1241c66a08d226f57d8d61e0ec519071c6afdb) feat(openapi): make open_api_override_responses act on default 404 response generation (#6551) +* [e09e73efc](https://github.com/api-platform/core/commit/e09e73efc5b4a39ab33d00c5d5422d8d9f7b5e89) feat: remove hydra prefix (#6418) + +## v3.4.0-alpha.5 + +### Deprecations: + +Namespaces like `ApiPlatform/Api` or `ApiPlatform/Util` are deprecated and will be removed in 4.0. +You should now install `api-platform/symfony` instead of `api-platform/core`. + +### Bug fixes + +* [17c6b586c](https://github.com/api-platform/core/commit/17c6b586c5ab49437ac11dd092efdd5f0baf569b) fix(state): log on missing provider (#6519) +* [601ccfb42](https://github.com/api-platform/core/commit/601ccfb4243803f40a7fa7179e0661da59c88b86) fix(doctrine): move event listeners to doctrine/common (#6573) +* [6499e0aa5](https://github.com/api-platform/core/commit/6499e0aa5dd61fdff7706e7940cdf8c1fc3e18ef) fix: deprecate url generator interface namespace (#6575) +* [3c5aea80f](https://github.com/api-platform/core/commit/3c5aea80fdbed20216764f6d721fe4f37cf2889d) fix(symfony): load isApiResource metadata (#6562) +* [61af0cc90](https://github.com/api-platform/core/commit/61af0cc90c1e095edb12e32ef433a742ef46637e) fix(doctrine): allow doctrine/dbal:^4 +* [e063b80af](https://github.com/api-platform/core/commit/e063b80afe012ca4a6c8999de55b59193e8ae0ae) fix: parameter context for filters (#6535) +* [e22392193](https://github.com/api-platform/core/commit/e22392193bb1fc71ece5abf393fa54b0745fc287) fix(state): security parameter with listeners (#6457) + +Various fixes for components isolation. + +### Features + +* [05e75be83](https://github.com/api-platform/core/commit/05e75be834c629e0487caaaedfe9fdf0bd5a7226) feat(doctrine): add new filter for filtering an entity using PHP backed enum, resolves #6506 (#6547) (#6560) +* [0b985ae76](https://github.com/api-platform/core/commit/0b985ae760bc4689d3f5bbacebb21b35b334d0be) feat(state): add security to parameters (#6435) +* [26d5cbb9b](https://github.com/api-platform/core/commit/26d5cbb9ba996f793e50eff3eab578ea0753d243) feat: deprecate query parameter validator (#6454) +* [63ccfd58c](https://github.com/api-platform/core/commit/63ccfd58c95b5aa4aa0353eb122d96ef35187222) feat: BackedEnum resources (#6309) +* [65296eaf1](https://github.com/api-platform/core/commit/65296eaf1eb18dc725e9316e9ab49b191aae43a3) feat(openapi): allow optional request body content (#6374) +* [7399fcf7e](https://github.com/api-platform/core/commit/7399fcf7eaf28cd649d137da1fdd54f69093e275) feat(symfony): skip error handler (#6463) +* [74986cb55](https://github.com/api-platform/core/commit/74986cb552182dc645bd1fc967faa0954dd59e0a) feat: inflector as service (#6447) +* [b47edb2a4](https://github.com/api-platform/core/commit/b47edb2a499c34e79c167f963e3a626a3e9d040a) feat(serializer): context IRI in HAL or JsonApi format (#6215) +* [db1241c66](https://github.com/api-platform/core/commit/db1241c66a08d226f57d8d61e0ec519071c6afdb) feat(openapi): make open_api_override_responses act on default 404 response generation (#6551) +* [e09e73efc](https://github.com/api-platform/core/commit/e09e73efc5b4a39ab33d00c5d5422d8d9f7b5e89) feat: remove hydra prefix (#6418) + +## v3.4.0-alpha.4 + +### Deprecations: + +Namespaces like `ApiPlatform/Api` or `ApiPlatform/Util` are deprecated and will be removed in 4.0. +You should now install `api-platform/symfony` instead of `api-platform/core`. + +### Bug fixes + +* [17c6b586c](https://github.com/api-platform/core/commit/17c6b586c5ab49437ac11dd092efdd5f0baf569b) fix(state): log on missing provider (#6519) +* [601ccfb42](https://github.com/api-platform/core/commit/601ccfb4243803f40a7fa7179e0661da59c88b86) fix(doctrine): move event listeners to doctrine/common (#6573) +* [6499e0aa5](https://github.com/api-platform/core/commit/6499e0aa5dd61fdff7706e7940cdf8c1fc3e18ef) fix: deprecate url generator interface namespace (#6575) +* [3c5aea80f](https://github.com/api-platform/core/commit/3c5aea80fdbed20216764f6d721fe4f37cf2889d) fix(symfony): load isApiResource metadata (#6562) +* [61af0cc90](https://github.com/api-platform/core/commit/61af0cc90c1e095edb12e32ef433a742ef46637e) fix(doctrine): allow doctrine/dbal:^4 +* [e063b80af](https://github.com/api-platform/core/commit/e063b80afe012ca4a6c8999de55b59193e8ae0ae) fix: parameter context for filters (#6535) +* [e22392193](https://github.com/api-platform/core/commit/e22392193bb1fc71ece5abf393fa54b0745fc287) fix(state): security parameter with listeners (#6457) + +### Features + +* [05e75be83](https://github.com/api-platform/core/commit/05e75be834c629e0487caaaedfe9fdf0bd5a7226) feat(doctrine): add new filter for filtering an entity using PHP backed enum, resolves #6506 (#6547) (#6560) +* [0b985ae76](https://github.com/api-platform/core/commit/0b985ae760bc4689d3f5bbacebb21b35b334d0be) feat(state): add security to parameters (#6435) +* [26d5cbb9b](https://github.com/api-platform/core/commit/26d5cbb9ba996f793e50eff3eab578ea0753d243) feat: deprecate query parameter validator (#6454) +* [63ccfd58c](https://github.com/api-platform/core/commit/63ccfd58c95b5aa4aa0353eb122d96ef35187222) feat: BackedEnum resources (#6309) +* [65296eaf1](https://github.com/api-platform/core/commit/65296eaf1eb18dc725e9316e9ab49b191aae43a3) feat(openapi): allow optional request body content (#6374) +* [7399fcf7e](https://github.com/api-platform/core/commit/7399fcf7eaf28cd649d137da1fdd54f69093e275) feat(symfony): skip error handler (#6463) +* [74986cb55](https://github.com/api-platform/core/commit/74986cb552182dc645bd1fc967faa0954dd59e0a) feat: inflector as service (#6447) +* [b47edb2a4](https://github.com/api-platform/core/commit/b47edb2a499c34e79c167f963e3a626a3e9d040a) feat(serializer): context IRI in HAL or JsonApi format (#6215) +* [db1241c66](https://github.com/api-platform/core/commit/db1241c66a08d226f57d8d61e0ec519071c6afdb) feat(openapi): make open_api_override_responses act on default 404 response generation (#6551) + +## v3.4.0-alpha.2 + +### Deprecations: + +Namespaces like `ApiPlatform/Api` or `ApiPlatform/Util` are deprecated and will be removed in 4.0. +You should now install `api-platform/symfony` instead of `api-platform/core`. + +### Bug fixes + +* [3c5aea80f](https://github.com/api-platform/core/commit/3c5aea80fdbed20216764f6d721fe4f37cf2889d) fix(symfony): load isApiResource metadata (#6562) +* [61af0cc90](https://github.com/api-platform/core/commit/61af0cc90c1e095edb12e32ef433a742ef46637e) fix(doctrine): allow doctrine/dbal:^4 +* [e063b80af](https://github.com/api-platform/core/commit/e063b80afe012ca4a6c8999de55b59193e8ae0ae) fix: parameter context for filters (#6535) +* [e22392193](https://github.com/api-platform/core/commit/e22392193bb1fc71ece5abf393fa54b0745fc287) fix(state): security parameter with listeners (#6457) + +### Features + +* [05e75be83](https://github.com/api-platform/core/commit/05e75be834c629e0487caaaedfe9fdf0bd5a7226) feat(doctrine): add new filter for filtering an entity using PHP backed enum, resolves #6506 (#6547) (#6560) +* [0b985ae76](https://github.com/api-platform/core/commit/0b985ae760bc4689d3f5bbacebb21b35b334d0be) feat(state): add security to parameters (#6435) +* [26d5cbb9b](https://github.com/api-platform/core/commit/26d5cbb9ba996f793e50eff3eab578ea0753d243) feat: deprecate query parameter validator (#6454) +* [63ccfd58c](https://github.com/api-platform/core/commit/63ccfd58c95b5aa4aa0353eb122d96ef35187222) feat: BackedEnum resources (#6309) +* [65296eaf1](https://github.com/api-platform/core/commit/65296eaf1eb18dc725e9316e9ab49b191aae43a3) feat(openapi): allow optional request body content (#6374) +* [7399fcf7e](https://github.com/api-platform/core/commit/7399fcf7eaf28cd649d137da1fdd54f69093e275) feat(symfony): skip error handler (#6463) +* [74986cb55](https://github.com/api-platform/core/commit/74986cb552182dc645bd1fc967faa0954dd59e0a) feat: inflector as service (#6447) +* [b47edb2a4](https://github.com/api-platform/core/commit/b47edb2a499c34e79c167f963e3a626a3e9d040a) feat(serializer): context IRI in HAL or JsonApi format (#6215) +* [db1241c66](https://github.com/api-platform/core/commit/db1241c66a08d226f57d8d61e0ec519071c6afdb) feat(openapi): make open_api_override_responses act on default 404 response generation (#6551) + +## v3.4.0-alpha.1 + +### Bug fixes + +* [41deeb4e4](https://github.com/api-platform/core/commit/41deeb4e49c35d27cd902ea41ddaa3c8c492d8e4) fix(symfony): fix debug:api-resource command for class with multiple resources with same uriTemplate (#6505) +* [52fd9818b](https://github.com/api-platform/core/commit/52fd9818b1d02dd89cac578e0762530079b5b42d) fix(elasticsearch): change normalize return type to compatible with other normalizers (#6493) +* [61af0cc90](https://github.com/api-platform/core/commit/61af0cc90c1e095edb12e32ef433a742ef46637e) fix(doctrine): allow doctrine/dbal:^4 +* [e22392193](https://github.com/api-platform/core/commit/e22392193bb1fc71ece5abf393fa54b0745fc287) fix(state): security parameter with listeners (#6457) + + +### Features + +* [0b985ae76](https://github.com/api-platform/core/commit/0b985ae760bc4689d3f5bbacebb21b35b334d0be) feat(state): add security to parameters (#6435) +* [26d5cbb9b](https://github.com/api-platform/core/commit/26d5cbb9ba996f793e50eff3eab578ea0753d243) feat: deprecate query parameter validator (#6454) +* [63ccfd58c](https://github.com/api-platform/core/commit/63ccfd58c95b5aa4aa0353eb122d96ef35187222) feat: BackedEnum resources (#6309) +* [65296eaf1](https://github.com/api-platform/core/commit/65296eaf1eb18dc725e9316e9ab49b191aae43a3) feat(openapi): allow optional request body content (#6374) +* [7399fcf7e](https://github.com/api-platform/core/commit/7399fcf7eaf28cd649d137da1fdd54f69093e275) feat(symfony): skip error handler (#6463) +* [74986cb55](https://github.com/api-platform/core/commit/74986cb552182dc645bd1fc967faa0954dd59e0a) feat: inflector as service (#6447) +* [b47edb2a4](https://github.com/api-platform/core/commit/b47edb2a499c34e79c167f963e3a626a3e9d040a) feat(serializer): context IRI in HAL or JsonApi format (#6215) + ## v3.3.15 ### Bug fixes -* [dc4fc84ba](https://github.com/api-platform/core/commit/dc4fc84ba93e22b4f44a37e90a93c6d079c1c620) fix(graphql): securityAfterResolver not called +* [dc4fc84ba](https://github.com/api-platform/core/commit/dc4fc84ba93e22b4f44a37e90a93c6d079c1c620) fix(graphql): securityAfterResolver not called * [9eb5c4e94](https://github.com/api-platform/core/commit/9eb5c4e941d0ebf59bc8ef5777b144db9b4a0899) fix(symfony): suggest `DocumentationAction` as replacement for deprecated `SwaggerUiAction` (#6894) ## v3.3.14 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cd3da9b2217..87e13b0ed6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,8 @@ Then, if it appears that it's a real bug, you may report it using GitHub by foll * A clear title to resume the issue * A description of the workflow needed to reproduce the bug -> _NOTE:_ Don't hesitate giving as much information as you can (OS, PHP version extensions...) +> [!NOTE] +> Don't hesitate giving as much information as you can (OS, PHP version extensions...) ## Pull Requests @@ -117,18 +118,19 @@ Only the first commit on a Pull Request need to use a conventional commit, other ### Tests -On `api-platform/core` there are two kinds of tests: unit (`phpunit` through `simple-phpunit`) and integration tests (`behat`). +On `api-platform/core` there are two kinds of tests: unit (`phpunit`) and integration tests (`behat`). Note that we stopped using `prophesize` for new tests since 3.2, use `phpunit` stub system. -Both `simple-phpunit` and `behat` are development dependencies and should be available in the `vendor` directory. +Both `phpunit` and `behat` are development dependencies and should be available in the `vendor` directory. Recommendations: * don't change existing tests if possible * always add a new `ApiResource` or a new `Entity/Document` to add a new test instead of changing an existing class -* as of API Platform 3 each component has it's own test directory, avoid the `tests/` directory except for functional tests +* as of API Platform 3 each component has its own test directory, avoid the `tests/` directory except for functional tests * dependencies between components must be kept at its minimal (`api-platform/metadata`, `api-platform/state`) except for bridges (Doctrine, Symfony, Laravel etc.) +* for functional testing with phpunit (see `tests/Functional`, add your ApiResource to `ApiPlatform\Tests\Fixtures\PhpUnitResourceNameCollectionFactory`) Note that in most of the testing, you don't need Doctrine take a look at how we write fixtures at: @@ -138,11 +140,11 @@ https://github.com/api-platform/core/blob/002c8b25283c9c06a085945f6206052a99a5fb To launch unit tests: - vendor/bin/simple-phpunit --stop-on-defect -vvv + vendor/bin/phpunit --stop-on-defect If you want coverage, you will need the `pcov` PHP extension and run: - vendor/bin/simple-phpunit --coverage-html coverage -vvv --stop-on-failure + vendor/bin/phpunit --coverage-html coverage --stop-on-defect Sometimes there might be an error with too many open files when generating coverage. To fix this, you can increase the `ulimit`, for example: @@ -152,6 +154,9 @@ Coverage will be available in `coverage/index.html`. #### Behat +> [!WARNING] +> Please **do not add new Behat tests**, use a functional test (for example: [ComputedFieldTest](https://github.com/api-platform/core/blob/04d5cff1b28b494ac2e90257a79ce6c045ba82ae/tests/Functional/Doctrine/ComputedFieldTest.php)). + The command to launch Behat tests is: php -d memory_limit=-1 ./vendor/bin/behat --profile=default --stop-on-failure --format=progress @@ -164,27 +169,68 @@ To get more details about an error, replace `--format=progress` by `-vvv`. You m docker run -p 27017:27017 mongo:latest -Start by adding a fixture, usually using Doctrine entities in `tests/Fixtures/TestBundle/Entity`. Note that we often duplicate the fixture -in the `tests/Fixtures/TestBundle/Document` directory for MongoDB ODM, if your test doesn't need to be tested with MongoDB use the `@!mongodb` group on the Behat scenario. -If you need a `Given` step, add it to the doctrine context in `tests/Core/Behat/DoctrineContext.php`, for example: +## Components tests + +> [!NOTE] +> This requires linking dependencies together, we recommend to use `composer global require --dev soyuka/pmu` (see [soyuka/pmu](github.com/soyuka/pmu)). + +API Platform is split into several components, components have their own set of tests. +To run a component test: + +```bash +COMPONENT_DIR=$(api-platform/doctrine-common cwd) +ROOT_DIR=$(pwd) +cd $COMPONENT_DIR +composer link $ROOT_DIR +./vendor/bin/phpunit ``` - /** - * @Given there is a payment - */ - public function thereIsAPayment() - { - $this->manager->persist(new Payment('123.45')); - $this->manager->flush(); - } + +
+ Run all the tests + + +```bash +#!/bin/bash + +set -e + +COMPONENTS=( + "api-platform/doctrine-common" + "api-platform/doctrine-orm" + "api-platform/doctrine-odm" + "api-platform/metadata" + "api-platform/hydra" + "api-platform/json-api" + "api-platform/json-schema" + "api-platform/elasticsearch" + "api-platform/openapi" + "api-platform/graphql" + "api-platform/http-cache" + "api-platform/ramsey-uuid" + "api-platform/serializer" + "api-platform/state" + "api-platform/symfony" + "api-platform/validator" +) + +PROJECT_ROOT=$(pwd) +for component in "${COMPONENTS[@]}"; do + cd "$PROJECT_ROOT" + find src -name vendor -exec rm -r {} \; || true + COMPONENT_DIR=$(composer "$component" --cwd) + cd $COMPONENT_DIR + composer link $PROJECT_ROOT + ./vendor/bin/phpunit --fail-on-deprecation --display-deprecations +done +exit 0 ``` +
-The last step is to add you feature inside `features/`. You can add your test in one of our existing features, or create your own. -## Components tests +## Changing a version constraint -API Platform is split into several components. There are tests for each of these, to run them `cd src/Doctrine/Common` then `composer update` and `./vendor/bin/phpunit`. -We do not provide a way to run all these tests at once yet. +Preferably change the version inside the root `composer.json`, then use `composer blend --all` to re-map the depedency accross each sub-project automatically. # License and Copyright Attribution @@ -200,7 +246,22 @@ If you include code from another project, please mention it in the Pull Request This section is for maintainers. 1. Update the JavaScript dependencies by running `./update-js.sh` (always check if it works in a browser) -2. Update the `CHANGELOG.md` file (be sure to include Pull Request numbers when appropriate) -3. Create a signed tag: `git tag -s vX.Y.Z -m "Version X.Y.Z"` -4. Create a release using GitHub's UI and copy the changelog -5. Create a new release of `api-platform/api-platform` +2. Update the `CHANGELOG.md` file (be sure to include Pull Request numbers when appropriate) we use: + +```bash +bash generate-changelog.sh v4.1.11 v4.1.12 > CHANGELOG.new +mv CHANGELOG.new CHANGELOG.md +``` +4. Update `composer.json` `version` node and use + +```bash +composer blend --json-path=version +``` + +This will update all the sub-components `composer.json`. + +4. Create a signed tag: `git tag -s vX.Y.Z -m "Version X.Y.Z"` +5. `git push upstream tag vX.Y.Z` +6. `gh release create --generate-notes vX.Y.Z` +7. Create a new release of `api-platform/api-platform` + diff --git a/behat.yml.dist b/behat.yml.dist index 887ae86f758..5f7419cf8c0 100644 --- a/behat.yml.dist +++ b/behat.yml.dist @@ -7,7 +7,6 @@ default: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -16,7 +15,7 @@ default: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: - tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@controller&&~@mercure' + tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@controller&&~@mercure&&~@query_parameter_validator' extensions: 'FriendsOfBehat\SymfonyExtension': bootstrap: 'tests/Fixtures/app/bootstrap.php' @@ -43,7 +42,6 @@ postgres: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -52,7 +50,7 @@ postgres: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: - tags: '~@sqlite&&~@mongodb&&~@elasticsearch&&~@controller&&~@mercure' + tags: '~@sqlite&&~@mongodb&&~@elasticsearch&&~@controller&&~@mercure&&~@query_parameter_validator' mongodb: suites: @@ -64,7 +62,6 @@ mongodb: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -73,7 +70,7 @@ mongodb: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: - tags: '~@sqlite&&~@elasticsearch&&~@!mongodb&&~@mercure&&~@controller' + tags: '~@sqlite&&~@elasticsearch&&~@!mongodb&&~@mercure&&~@controller&&~@query_parameter_validator' mercure: suites: @@ -85,7 +82,6 @@ mercure: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -109,7 +105,7 @@ elasticsearch: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: - tags: '@elasticsearch&&~@mercure' + tags: '@elasticsearch&&~@mercure&&~@query_parameter_validator' default-coverage: suites: @@ -120,7 +116,6 @@ default-coverage: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -141,7 +136,6 @@ mongodb-coverage: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -162,7 +156,6 @@ mercure-coverage: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -194,7 +187,6 @@ legacy: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -203,7 +195,7 @@ legacy: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: - tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@link_security&&~@use_listener' + tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@link_security&&~@use_listener&&~@query_parameter_validator' extensions: 'FriendsOfBehat\SymfonyExtension': bootstrap: 'tests/Fixtures/app/bootstrap.php' @@ -229,7 +221,6 @@ symfony_listeners: - 'ApiPlatform\Tests\Behat\GraphqlContext' - 'ApiPlatform\Tests\Behat\JsonContext' - 'ApiPlatform\Tests\Behat\HydraContext' - - 'ApiPlatform\Tests\Behat\OpenApiContext' - 'ApiPlatform\Tests\Behat\HttpCacheContext' - 'ApiPlatform\Tests\Behat\JsonApiContext' - 'ApiPlatform\Tests\Behat\JsonHalContext' @@ -238,7 +229,7 @@ symfony_listeners: - 'Behat\MinkExtension\Context\MinkContext' - 'behatch:context:rest' filters: - tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@mercure' + tags: '~@postgres&&~@mongodb&&~@elasticsearch&&~@mercure&&~@query_parameter_validator' extensions: 'FriendsOfBehat\SymfonyExtension': bootstrap: 'tests/Fixtures/app/bootstrap.php' diff --git a/composer.json b/composer.json index 4ae60cda619..605e82a5e74 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,71 @@ { - "name": "api-platform/core", + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr/" + } + ], + "autoload": { + "psr-4": { + "ApiPlatform\\": "src/" + }, + "files": [ + "src/JsonLd/HydraContext.php" + ] + }, + "autoload-dev": { + "psr-4": { + "ApiPlatform\\Tests\\": "tests/", + "App\\": "tests/Fixtures/app/var/tmp/src/" + } + }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "phpstan/extension-installer": true, + "php-http/discovery": true, + "soyuka/pmu": true + } + }, + "conflict": { + "doctrine/common": "<3.2.2", + "doctrine/dbal": "<2.10", + "doctrine/orm": "<2.14.0", + "doctrine/mongodb-odm": "<2.4", + "doctrine/persistence": "<1.3", + "symfony/framework-bundle": "6.4.6 || 7.0.6", + "symfony/var-exporter": "<6.1.1", + "phpunit/phpunit": "<9.5", + "phpspec/prophecy": "<1.15" + }, "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", - "type": "library", + "extra": { + "branch-alias": { + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-main": "4.3.x-dev" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "pmu": { + "projects": [ + "./src/*/composer.json", + "src/Doctrine/*/composer.json" + ] + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" + } + }, + "homepage": "https://api-platform.com/", "keywords": [ "REST", "GraphQL", @@ -11,63 +75,92 @@ "JSONAPI", "OpenAPI", "HAL", - "Swagger" + "Swagger", + "Symfony", + "Laravel" ], - "homepage": "https://api-platform.com/", "license": "MIT", - "authors": [ - { - "name": "Kévin Dunglas", - "email": "kevin@dunglas.fr", - "homepage": "https://dunglas.fr/" - } - ], + "name": "api-platform/core", + "replace": { + "api-platform/doctrine-common": "self.version", + "api-platform/doctrine-odm": "self.version", + "api-platform/doctrine-orm": "self.version", + "api-platform/documentation": "self.version", + "api-platform/elasticsearch": "self.version", + "api-platform/graphql": "self.version", + "api-platform/http-cache": "self.version", + "api-platform/hydra": "self.version", + "api-platform/json-api": "self.version", + "api-platform/json-hal": "self.version", + "api-platform/json-schema": "self.version", + "api-platform/jsonld": "self.version", + "api-platform/laravel": "self.version", + "api-platform/metadata": "self.version", + "api-platform/openapi": "self.version", + "api-platform/parameter-validator": "self.version", + "api-platform/ramsey-uuid": "self.version", + "api-platform/serializer": "self.version", + "api-platform/state": "self.version", + "api-platform/symfony": "self.version", + "api-platform/validator": "self.version" + }, "require": { - "php": ">=8.1", - "doctrine/inflector": "^1.0 || ^2.0", + "php": ">=8.2", + "doctrine/inflector": "^2.0", "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.0 || ^2.0", "symfony/deprecation-contracts": "^3.1", "symfony/http-foundation": "^6.4 || ^7.0", "symfony/http-kernel": "^6.4 || ^7.0", "symfony/property-access": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.1", "symfony/serializer": "^6.4 || ^7.0", "symfony/translation-contracts": "^3.3", - "symfony/web-link": "^6.4 || ^7.0", - "willdurand/negotiation": "^3.0" + "symfony/type-info": "^7.3 || 7.4.x-dev", + "symfony/validator": "^6.4 || ^7.1", + "symfony/web-link": "^6.4 || ^7.1", + "willdurand/negotiation": "^3.1" }, "require-dev": { + "ext-mongodb": "^1.21 || ^2.0", "behat/behat": "^3.11", "behat/mink": "^1.9", "doctrine/cache": "^1.11 || ^2.1", "doctrine/common": "^3.2.2", - "doctrine/dbal": "^3.4.0", - "doctrine/doctrine-bundle": "^1.12 || ^2.0", - "doctrine/mongodb-odm": "^2.2", - "doctrine/mongodb-odm-bundle": "^4.0 || ^5.0", - "doctrine/orm": "^2.14 || ^3.0", - "elasticsearch/elasticsearch": "^7.11 || ^8.4", + "doctrine/dbal": "^4.0", + "doctrine/doctrine-bundle": "^2.11", + "doctrine/mongodb-odm": "^2.10", + "doctrine/mongodb-odm-bundle": "^5.0", + "doctrine/orm": "^2.17 || ^3.0", + "elasticsearch/elasticsearch": "^7.17 || ^8.4 || ^9.0", "friends-of-behat/mink-browserkit-driver": "^1.3.1", "friends-of-behat/mink-extension": "^2.2", "friends-of-behat/symfony-extension": "^2.1", "guzzlehttp/guzzle": "^6.0 || ^7.0", - "jangregor/phpstan-prophecy": "^1.0", - "justinrainbow/json-schema": "^5.2.1", - "phpspec/prophecy-phpunit": "^2.0", + "illuminate/config": "^11.0 || ^12.0", + "illuminate/contracts": "^11.0 || ^12.0", + "illuminate/database": "^11.0 || ^12.0", + "illuminate/http": "^11.0 || ^12.0", + "illuminate/pagination": "^11.0 || ^12.0", + "illuminate/routing": "^11.0 || ^12.0", + "illuminate/support": "^11.0 || ^12.0", + "jangregor/phpstan-prophecy": "^2.1.11", + "justinrainbow/json-schema": "^5.2.11", + "laravel/framework": "^11.0 || ^12.0", + "orchestra/testbench": "^9.1", + "phpspec/prophecy-phpunit": "^2.2", "phpstan/extension-installer": "^1.1", - "phpstan/phpdoc-parser": "^1.13", - "phpstan/phpstan": "^1.10", - "phpstan/phpstan-doctrine": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", - "phpstan/phpstan-symfony": "^1.0", - "phpunit/phpunit": "^9.6", + "phpstan/phpdoc-parser": "^1.29 || ^2.0", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-doctrine": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-symfony": "^2.0", + "phpunit/phpunit": "11.5.x-dev", "psr/log": "^1.0 || ^2.0 || ^3.0", - "ramsey/uuid": "^3.9.7 || ^4.0", - "ramsey/uuid-doctrine": "^1.4 || ^2.0", - "sebastian/comparator": "<5.0", - "soyuka/contexts": "v3.3.9", - "soyuka/pmu": "^0.0.2", + "ramsey/uuid": "^4.7", + "ramsey/uuid-doctrine": "^2.0", + "soyuka/contexts": "^3.3.10", + "soyuka/pmu": "^0.2.0", "soyuka/stubs-mongodb": "^1.0", "symfony/asset": "^6.4 || ^7.0", "symfony/browser-kit": "^6.4 || ^7.0", @@ -75,49 +168,38 @@ "symfony/config": "^6.4 || ^7.0", "symfony/console": "^6.4 || ^7.0", "symfony/css-selector": "^6.4 || ^7.0", - "symfony/dependency-injection": "^6.4 || ^7.0.12", - "symfony/doctrine-bridge": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0", + "symfony/doctrine-bridge": "^6.4.2 || ^7.1", "symfony/dom-crawler": "^6.4 || ^7.0", "symfony/error-handler": "^6.4 || ^7.0", "symfony/event-dispatcher": "^6.4 || ^7.0", "symfony/expression-language": "^6.4 || ^7.0", "symfony/finder": "^6.4 || ^7.0", "symfony/form": "^6.4 || ^7.0", - "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/framework-bundle": "^6.4 || ^7.0 || 7.4.x-dev", "symfony/http-client": "^6.4 || ^7.0", "symfony/intl": "^6.4 || ^7.0", + "symfony/json-streamer": "7.4.x-dev", "symfony/maker-bundle": "^1.24", "symfony/mercure-bundle": "*", "symfony/messenger": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4.1 || ^7.0", + "symfony/object-mapper": "7.4.x-dev", "symfony/routing": "^6.4 || ^7.0", "symfony/security-bundle": "^6.4 || ^7.0", "symfony/security-core": "^6.4 || ^7.0", "symfony/stopwatch": "^6.4 || ^7.0", + "symfony/string": "^6.4 || ^7.0", "symfony/twig-bundle": "^6.4 || ^7.0", "symfony/uid": "^6.4 || ^7.0", "symfony/validator": "^6.4 || ^7.0", "symfony/web-profiler-bundle": "^6.4 || ^7.0", "symfony/yaml": "^6.4 || ^7.0", "twig/twig": "^1.42.3 || ^2.12 || ^3.0", - "webonyx/graphql-php": "^14.0 || ^15.0" - }, - "conflict": { - "doctrine/common": "<3.2.2", - "doctrine/dbal": "<2.10", - "doctrine/orm": "<2.14.0", - "doctrine/mongodb-odm": "<2.4", - "doctrine/persistence": "<1.3", - "symfony/framework-bundle": "6.4.6 || 7.0.6", - "symfony/var-exporter": "<6.1.1", - "phpunit/phpunit": "<9.5", - "phpspec/prophecy": "<1.15", - "elasticsearch/elasticsearch": ">=8.0,<8.4" + "webonyx/graphql-php": "^15.0" }, "suggest": { "doctrine/mongodb-odm-bundle": "To support MongoDB. Only versions 4.0 and later are supported.", "elasticsearch/elasticsearch": "To support Elasticsearch.", - "ocramius/package-versions": "To display the API Platform's version in the debug bar.", "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc.", "psr/cache-implementation": "To use metadata caching.", "ramsey/uuid": "To support Ramsey's UUID identifiers.", @@ -130,77 +212,11 @@ "symfony/uid": "To support Symfony UUID/ULID identifiers.", "symfony/messenger": "To support messenger integration.", "symfony/web-profiler-bundle": "To use the data collector.", + "symfony/json-streamer": "To use the JSON Streamer component.", "webonyx/graphql-php": "To support GraphQL." }, - "autoload": { - "psr-4": { - "ApiPlatform\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "ApiPlatform\\Tests\\": "tests/", - "App\\": "tests/Fixtures/app/var/tmp/src/" - } - }, - "config": { - "preferred-install": { - "*": "dist" - }, - "sort-packages": true, - "allow-plugins": { - "composer/package-versions-deprecated": true, - "phpstan/extension-installer": true, - "php-http/discovery": true, - "soyuka/pmu": true - } - }, - "extra": { - "branch-alias": { - "dev-main": "3.3.x-dev" - }, - "symfony": { - "require": "^6.4 || ^7.0" - }, - "projects": [ - "api-platform/doctrine-common", - "api-platform/doctrine-orm", - "api-platform/doctrine-odm", - "api-platform/metadata", - "api-platform/json-schema", - "api-platform/elasticsearch", - "api-platform/jsonld", - "api-platform/hydra", - "api-platform/openapi", - "api-platform/graphql", - "api-platform/http-cache", - "api-platform/documentation", - "api-platform/parameter-validator", - "api-platform/ramsey-uuid", - "api-platform/serializer", - "api-platform/state", - "api-platform/symfony", - "api-platform/validator" - ] - }, + "type": "library", "repositories": [ - {"type": "path", "url": "./src/Doctrine/Common"}, - {"type": "path", "url": "./src/Doctrine/Orm"}, - {"type": "path", "url": "./src/Doctrine/Odm"}, - {"type": "path", "url": "./src/Metadata"}, - {"type": "path", "url": "./src/JsonSchema"}, - {"type": "path", "url": "./src/Elasticsearch"}, - {"type": "path", "url": "./src/JsonLd"}, - {"type": "path", "url": "./src/Hydra"}, - {"type": "path", "url": "./src/OpenApi"}, - {"type": "path", "url": "./src/GraphQl"}, - {"type": "path", "url": "./src/HttpCache"}, - {"type": "path", "url": "./src/Documentation"}, - {"type": "path", "url": "./src/ParameterValidator"}, - {"type": "path", "url": "./src/RamseyUuid"}, - {"type": "path", "url": "./src/Serializer"}, - {"type": "path", "url": "./src/State"}, - {"type": "path", "url": "./src/Symfony"}, - {"type": "path", "url": "./src/Validator"} + {"type": "vcs", "url": "https://github.com/soyuka/phpunit"} ] } diff --git a/docs/adr/0005-refactor-state-management.md b/docs/adr/0005-refactor-state-management.md index a4196044dab..f5549e17d64 100644 --- a/docs/adr/0005-refactor-state-management.md +++ b/docs/adr/0005-refactor-state-management.md @@ -30,7 +30,7 @@ For API Platform 3, we refactored the whole metadata susbsytem to be more flexib This led to the refactoring of the two main interfaces allowing to plug a data source in API Platform: the state provider and the state processor interfaces. Leveraging these new interfaces, it should be possible to simplify the code base and to remove most code duplication by transforming most of the code currently -stored in the kernel event listeners and in the GraphQL resolvers in dedicated state processors and state providers. +stored in the kernel event listeners and in the GraphQL resolvers in dedicated state processors and state providers. This is quite close to what @alanpoulain proposed in 2019 at https://github.com/api-platform/core/pull/2978 although at that time we needed to refactor the subresource system before tackling this issue. ## Decision Outcome diff --git a/docs/adr/0006-filtering-system-and-parameters.md b/docs/adr/0006-filtering-system-and-parameters.md index c1accc30412..0f81cc55cbf 100644 --- a/docs/adr/0006-filtering-system-and-parameters.md +++ b/docs/adr/0006-filtering-system-and-parameters.md @@ -193,8 +193,8 @@ During the `Provider` phase (`RequestEvent::REQUEST`), we could use a `Parameter interface ParameterProviderInterface { /** - * @param array $parameters - * @param array|array{request?: Request, resource_class?: string, operation: HttpOperation} $context + * @param array $parameters + * @param array $context */ public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?HttpOperation; } diff --git a/docs/composer.json b/docs/composer.json index c72780bbf30..bbc61158231 100644 --- a/docs/composer.json +++ b/docs/composer.json @@ -15,7 +15,8 @@ } ], "require": { - "api-platform/core": "dev-main", + "api-platform/symfony": "^3.4 || ^4.0", + "api-platform/doctrine-orm": "^3.4 || ^4.0", "symfony/expression-language": "^7.0", "nelmio/cors-bundle": "^2.2", "phpstan/phpdoc-parser": "^1.15", @@ -24,6 +25,7 @@ "symfony/property-info": "^7.0", "symfony/runtime": "^7.0", "symfony/security-bundle": "^7.0", + "symfony/type-info": "^7.3-dev", "symfony/serializer": "^7.0", "symfony/validator": "^7.0", "symfony/yaml": "^7.0", @@ -34,8 +36,7 @@ "zenstruck/foundry": "^1.31", "symfony/http-client": "^7.0", "symfony/browser-kit": "^7.0", - "justinrainbow/json-schema": "^5.2", - "webonyx/graphql-php": "^15.11" + "justinrainbow/json-schema": "^5.2" }, "config": { "allow-plugins": { @@ -44,5 +45,17 @@ }, "require-dev": { "phpunit/phpunit": "^10" - } + }, + "extra": { + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" + } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/symfony/type-info" + } + ] } diff --git a/docs/config/packages/framework.yaml b/docs/config/packages/framework.yaml index 3cc9e1ea7d3..e930a6e102d 100644 --- a/docs/config/packages/framework.yaml +++ b/docs/config/packages/framework.yaml @@ -7,13 +7,11 @@ api_platform: json: ['application/json'] docs_formats: jsonopenapi: ['application/vnd.openapi+json'] - keep_legacy_inflector: false http_cache: - invalidation: - enabled: true - public: true + invalidation: + enabled: true + public: true use_symfony_listeners: false defaults: extra_properties: - rfc_7807_compliant_errors: true standard_put: true diff --git a/docs/guides/computed-field.php b/docs/guides/computed-field.php new file mode 100644 index 00000000000..3b3fff27e61 --- /dev/null +++ b/docs/guides/computed-field.php @@ -0,0 +1,324 @@ +getValue() instanceof ParameterNotFound) { + return; + } + + // Extract the desired sort direction ('asc' or 'desc') from the parameter's value. + // IMPORTANT: 'totalQuantity' here MUST match the alias defined in Cart::handleLinks. + $queryBuilder->addOrderBy('totalQuantity', $context['parameter']->getValue()['totalQuantity'] ?? 'ASC'); + } + + /** + * @return array + */ + // Defines the OpenAPI/Swagger schema for this filter parameter. + // Tells API Platform documentation generators that 'sort[totalQuantity]' expects 'asc' or 'desc'. + // This also add constraint violations to the parameter that will reject any wrong values. + public function getSchema(Parameter $parameter): array + { + return ['type' => 'string', 'enum' => ['asc', 'desc']]; + } + + public function getDescription(string $resourceClass): array + { + return []; + } + } +} + +namespace App\Entity { + use ApiPlatform\Doctrine\Orm\State\Options; + use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; + use ApiPlatform\Metadata\GetCollection; + use ApiPlatform\Metadata\NotExposed; + use ApiPlatform\Metadata\Operation; + use ApiPlatform\Metadata\QueryParameter; + use App\Filter\SortComputedFieldFilter; + use Doctrine\Common\Collections\ArrayCollection; + use Doctrine\Common\Collections\Collection; + use Doctrine\ORM\Mapping as ORM; + use Doctrine\ORM\QueryBuilder; + + #[ORM\Entity] + // Defines the GetCollection operation for Cart, including computed 'totalQuantity'. + // Recipe involves: + // 1. handleLinks (modify query) + // 2. process (map result) + // 3. parameters (filters) + #[GetCollection( + normalizationContext: ['hydra_prefix' => false], + paginationItemsPerPage: 3, + paginationPartial: false, + // stateOptions: Uses handleLinks to modify the query *before* fetching. + stateOptions: new Options(handleLinks: [self::class, 'handleLinks']), + // processor: Uses process to map the result *after* fetching, *before* serialization. + processor: [self::class, 'process'], + write: true, + // parameters: Defines query parameters. + parameters: [ + // Define the sorting parameter for 'totalQuantity'. + 'sort[:property]' => new QueryParameter( + // Link this parameter definition to our custom filter. + filter: new SortComputedFieldFilter(), + // Specify which properties this filter instance should handle. + properties: ['totalQuantity'], + property: 'totalQuantity' + ), + ] + )] + class Cart + { + // Handles links/joins and modifications to the QueryBuilder *before* data is fetched (via stateOptions). + // Adds SQL logic (JOIN, SELECT aggregate, GROUP BY) to calculate 'totalQuantity' at the database level. + // The alias 'totalQuantity' created here is crucial for the filter and processor. + public static function handleLinks(QueryBuilder $queryBuilder, array $uriVariables, QueryNameGeneratorInterface $queryNameGenerator, array $context): void + { + // Get the alias for the root entity (Cart), usually 'o'. + $rootAlias = $queryBuilder->getRootAliases()[0] ?? 'o'; + // Generate a unique alias for the joined 'items' relation to avoid conflicts. + $itemsAlias = $queryNameGenerator->generateParameterName('items'); + $queryBuilder->leftJoin(\sprintf('%s.items', $rootAlias), $itemsAlias) + ->addSelect(\sprintf('COALESCE(SUM(%s.quantity), 0) AS totalQuantity', $itemsAlias)) + ->addGroupBy(\sprintf('%s.id', $rootAlias)); + } + + // Processor function called *after* fetching data, *before* serialization. + // Maps the raw 'totalQuantity' from Doctrine result onto the Cart entity's property. + // Handles Doctrine's array result structure: [0 => Entity, 'alias' => computedValue]. + // Reshapes data back into an array of Cart objects. + public static function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) + { + // Iterate through the raw results. $value will be like [0 => Cart Object, 'totalQuantity' => 15] + foreach ($data as &$value) { + // Get the Cart entity object. + $cart = $value[0]; + // Get the computed totalQuantity value using the alias defined in handleLinks. + // Use null coalescing operator for safety. + $cart->totalQuantity = $value['totalQuantity'] ?? 0; + // Replace the raw array structure with just the processed Cart object. + $value = $cart; + } + + // Return the collection of Cart objects with the totalQuantity property populated. + return $data; + } + + // Public property to hold the computed total quantity. + // Not mapped by Doctrine (@ORM\Column) but populated by the 'process' method. + // API Platform will serialize this property. + public ?int $totalQuantity; + + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private ?int $id = null; + + /** + * @var Collection the items in this cart + */ + #[ORM\OneToMany(targetEntity: CartProduct::class, mappedBy: 'cart', cascade: ['persist', 'remove'], orphanRemoval: true)] + private Collection $items; + + public function __construct() + { + $this->items = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + /** + * @return Collection + */ + public function getItems(): Collection + { + return $this->items; + } + + public function addItem(CartProduct $item): self + { + if (!$this->items->contains($item)) { + $this->items[] = $item; + $item->setCart($this); + } + + return $this; + } + + public function removeItem(CartProduct $item): self + { + if ($this->items->removeElement($item)) { + // set the owning side to null (unless already changed) + if ($item->getCart() === $this) { + $item->setCart(null); + } + } + + return $this; + } + } + + #[NotExposed()] + #[ORM\Entity] + class CartProduct + { + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private ?int $id = null; + + #[ORM\ManyToOne(targetEntity: Cart::class, inversedBy: 'items')] + #[ORM\JoinColumn(nullable: false)] + private ?Cart $cart = null; + + #[ORM\Column(type: 'integer')] + private int $quantity = 1; + + public function getId(): ?int + { + return $this->id; + } + + public function getCart(): ?Cart + { + return $this->cart; + } + + public function setCart(?Cart $cart): self + { + $this->cart = $cart; + + return $this; + } + + public function getQuantity(): int + { + return $this->quantity; + } + + public function setQuantity(int $quantity): self + { + $this->quantity = $quantity; + + return $this; + } + } +} + +namespace App\Playground { + use Symfony\Component\HttpFoundation\Request; + + function request(): Request + { + return Request::create('/carts?sort[totalQuantity]=asc', 'GET'); + } +} + +namespace DoctrineMigrations { + use Doctrine\DBAL\Schema\Schema; + use Doctrine\Migrations\AbstractMigration; + + final class Migration extends AbstractMigration + { + public function up(Schema $schema): void + { + $this->addSql('CREATE TABLE cart (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)'); + $this->addSql('CREATE TABLE cart_product (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, quantity INTEGER NOT NULL, cart_id INTEGER NOT NULL, CONSTRAINT FK_6DDC373A1AD5CDBF FOREIGN KEY (cart_id) REFERENCES cart (id) NOT DEFERRABLE INITIALLY IMMEDIATE)'); + $this->addSql('CREATE INDEX IDX_6DDC373A1AD5CDBF ON cart_product (cart_id)'); + } + } +} + +namespace App\Tests { + use ApiPlatform\Playground\Test\TestGuideTrait; + use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; + + final class ComputedFieldTest extends ApiTestCase + { + use TestGuideTrait; + + public function testCanSortByComputedField(): void + { + $ascReq = static::createClient()->request('GET', '/carts?sort[totalQuantity]=asc'); + $this->assertResponseIsSuccessful(); + $asc = $ascReq->toArray(); + $this->assertGreaterThan( + $asc['member'][0]['totalQuantity'], + $asc['member'][1]['totalQuantity'] + ); + } + } +} + +namespace App\Fixtures { + use App\Entity\Cart; + use App\Entity\CartProduct; + use Doctrine\Bundle\FixturesBundle\Fixture; + use Doctrine\Persistence\ObjectManager; + + use function Zenstruck\Foundry\anonymous; + use function Zenstruck\Foundry\repository; + + final class CartFixtures extends Fixture + { + public function load(ObjectManager $manager): void + { + $cartFactory = anonymous(Cart::class); + if (repository(Cart::class)->count()) { + return; + } + + $cartFactory->many(10)->create(fn ($i) => [ + 'items' => $this->createCartProducts($i), + ]); + } + + /** + * @return array + */ + private function createCartProducts($i): array + { + $cartProducts = []; + for ($j = 1; $j <= 10; ++$j) { + $cartProduct = new CartProduct(); + $cartProduct->setQuantity((int) abs($j / $i) + 1); + $cartProducts[] = $cartProduct; + } + + return $cartProducts; + } + } +} diff --git a/docs/guides/create-a-custom-doctrine-filter.php b/docs/guides/create-a-custom-doctrine-filter.php index 32af2c2bf25..72a6dbde456 100644 --- a/docs/guides/create-a-custom-doctrine-filter.php +++ b/docs/guides/create-a-custom-doctrine-filter.php @@ -26,7 +26,6 @@ use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; - use Symfony\Component\PropertyInfo\Type; final class RegexpFilter extends AbstractFilter { @@ -67,7 +66,7 @@ public function getDescription(string $resourceClass): array foreach ($this->properties as $property => $strategy) { $description["regexp_$property"] = [ 'property' => $property, - 'type' => Type::BUILTIN_TYPE_STRING, + 'type' => 'string', 'required' => false, 'description' => 'Filter using a regex. This will appear in the OpenAPI documentation!', 'openapi' => [ @@ -153,11 +152,11 @@ public function testAsAnonymousICanAccessTheDocumentation(): void $this->assertResponseIsSuccessful(); $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection'); $this->assertJsonContains([ - 'hydra:search' => [ - '@type' => 'hydra:IriTemplate', - 'hydra:template' => '/books.jsonld{?regexp_title,regexp_author}', - 'hydra:variableRepresentation' => 'BasicRepresentation', - 'hydra:mapping' => [ + 'search' => [ + '@type' => 'IriTemplate', + 'template' => '/books.jsonld{?regexp_title,regexp_author}', + 'variableRepresentation' => 'BasicRepresentation', + 'mapping' => [ [ '@type' => 'IriTemplateMapping', 'variable' => 'regexp_title', diff --git a/docs/guides/custom-pagination.php b/docs/guides/custom-pagination.php index 85187f5c0c8..a83bac18724 100644 --- a/docs/guides/custom-pagination.php +++ b/docs/guides/custom-pagination.php @@ -7,7 +7,7 @@ // tags: expert // --- -// In case you're using a custom collection (through a Provider), make sure you return the `Paginator` object to get the full hydra response with `hydra:view` (which contains information about first, last, next and previous page). +// In case you're using a custom collection (through a Provider), make sure you return the `Paginator` object to get the full hydra response with `view` (which contains information about first, last, next and previous page). // // The following example shows how to handle it using a custom Provider. You will need to use the Doctrine Paginator and pass it to the API Platform Paginator. @@ -160,15 +160,15 @@ public function testTheCustomCollectionIsPaginated(): void $this->assertResponseIsSuccessful(); $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection', 'jsonld'); - $this->assertNotSame(0, $response->toArray(false)['hydra:totalItems'], 'The collection is empty.'); + $this->assertNotSame(0, $response->toArray(false)['totalItems'], 'The collection is empty.'); $this->assertJsonContains([ - 'hydra:totalItems' => 35, - 'hydra:view' => [ + 'totalItems' => 35, + 'view' => [ '@id' => '/books.jsonld?page=1', - '@type' => 'hydra:PartialCollectionView', - 'hydra:first' => '/books.jsonld?page=1', - 'hydra:last' => '/books.jsonld?page=2', - 'hydra:next' => '/books.jsonld?page=2', + '@type' => 'PartialCollectionView', + 'first' => '/books.jsonld?page=1', + 'last' => '/books.jsonld?page=2', + 'next' => '/books.jsonld?page=2', ], ]); } diff --git a/docs/guides/delete-operation-with-validation.php b/docs/guides/delete-operation-with-validation.php index a8d285b85a9..017e73002fa 100644 --- a/docs/guides/delete-operation-with-validation.php +++ b/docs/guides/delete-operation-with-validation.php @@ -15,7 +15,7 @@ #[\Attribute] class AssertCanDelete extends Constraint { - public string $message = 'For whatever reason we denied removeal of this data.'; + public string $message = 'For whatever reason we denied removal of this data.'; public string $mode = 'strict'; public function getTargets(): string diff --git a/docs/guides/doctrine-search-filter.php b/docs/guides/doctrine-search-filter.php index d9f0cd21abb..bcf94c57336 100644 --- a/docs/guides/doctrine-search-filter.php +++ b/docs/guides/doctrine-search-filter.php @@ -118,21 +118,21 @@ public function testGetDocumentation(): void $this->assertResponseIsSuccessful(); $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_books{._format}_get_collection', 'jsonld'); $this->assertJsonContains([ - 'hydra:search' => [ - '@type' => 'hydra:IriTemplate', - 'hydra:template' => '/books.jsonld{?author,title}', - 'hydra:variableRepresentation' => 'BasicRepresentation', - 'hydra:mapping' => [ + 'search' => [ + '@type' => 'IriTemplate', + 'template' => '/books.jsonld{?title,author}', + 'variableRepresentation' => 'BasicRepresentation', + 'mapping' => [ [ '@type' => 'IriTemplateMapping', - 'variable' => 'author', - 'property' => 'author', + 'variable' => 'title', + 'property' => 'title', 'required' => false, ], [ '@type' => 'IriTemplateMapping', - 'variable' => 'title', - 'property' => 'title', + 'variable' => 'author', + 'property' => 'author', 'required' => false, ], ], diff --git a/docs/guides/error-provider.php b/docs/guides/error-provider.php index 5a272ce4f4d..e417904008b 100644 --- a/docs/guides/error-provider.php +++ b/docs/guides/error-provider.php @@ -7,14 +7,6 @@ // tags: design, state // --- -// Note that we use the following configuration: -// ``` -// api_platform: -// defaults: -// rfc_7807_compliant_errors: true -// ``` -// To customize the API Platform response, replace the api_platform.state.error_provider with your own provider: - namespace App\ApiResource { use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; diff --git a/docs/guides/error-resource.php b/docs/guides/error-resource.php index 8ce945df841..aee265e2831 100644 --- a/docs/guides/error-resource.php +++ b/docs/guides/error-resource.php @@ -7,13 +7,6 @@ // tags: design, state // --- -// Note that we use the following configuration: -// ``` -// api_platform: -// defaults: -// rfc_7807_compliant_errors: true -// ``` - namespace App\ApiResource { use ApiPlatform\Metadata\ErrorResource; use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; @@ -25,12 +18,12 @@ class MyDomainException extends \Exception implements ProblemExceptionInterface { public function getType(): string { - return 'teapot'; + return '/errors/418'; } public function getTitle(): ?string { - return null; + return 'Teapot error'; } public function getStatus(): ?int @@ -47,6 +40,8 @@ public function getInstance(): ?string { return null; } + + public string $myCustomField = 'I usually prefer coffee.'; } use ApiPlatform\Metadata\ApiResource; @@ -90,7 +85,12 @@ public function testBookDoesNotExists(): void // you can override this by looking at the [Error Provider guide](/docs/guides/error-provider). $this->assertResponseStatusCodeSame(418); $this->assertJsonContains([ + '@id' => '/my_domain_exceptions', + '@type' => 'MyDomainException', + 'type' => '/errors/418', + 'title' => 'Teapot error', 'detail' => 'I am teapot', + 'myCustomField' => 'I usually prefer coffee.' ]); } } diff --git a/docs/guides/how-to.php b/docs/guides/how-to.php index d9999dac1a9..79aaa2a99c1 100644 --- a/docs/guides/how-to.php +++ b/docs/guides/how-to.php @@ -137,9 +137,9 @@ public function testAsAnonymousICanAccessTheDocumentation(): void $this->assertResponseIsSuccessful(); $this->assertMatchesResourceCollectionJsonSchema(Book::class, '_api_/books{._format}_get_collection', 'jsonld'); - $this->assertNotSame(0, $response->toArray(false)['hydra:totalItems'], 'The collection is empty.'); + $this->assertNotSame(0, $response->toArray(false)['totalItems'], 'The collection is empty.'); $this->assertJsonContains([ - 'hydra:totalItems' => 10, + 'totalItems' => 10, ]); } } diff --git a/docs/guides/return-the-iri-of-your-resources-relations.php b/docs/guides/return-the-iri-of-your-resources-relations.php index e8fec5cb639..41fb3b779a9 100644 --- a/docs/guides/return-the-iri-of-your-resources-relations.php +++ b/docs/guides/return-the-iri-of-your-resources-relations.php @@ -157,7 +157,7 @@ public function setBrand(Brand $brand): void // // The **OpenAPI** documentation will set the properties as `read-only` of type `string` in the format `iri-reference` for `JSON-LD`, `JSON:API` and `HAL` formats. // -// The **Hydra** documentation will set the properties as `hydra:Link` with the right domain, with `hydra:readable` to `true` but `hydra:writable` to `false`. +// The **Hydra** documentation will set the properties as `Link` with the right domain, with `readable` to `true` but `writable` to `false`. // // When using JSON:API or HAL formats, the IRI will be used and set links, embedded and relationship. // diff --git a/docs/guides/test-your-api.php b/docs/guides/test-your-api.php index 7749ccc920c..c0fea83ace5 100644 --- a/docs/guides/test-your-api.php +++ b/docs/guides/test-your-api.php @@ -39,7 +39,7 @@ public function testGetCollection(): void // matches an expected format, for example here with a collection. $this->assertMatchesResourceCollectionJsonSchema(Book::class); // PHPUnit default assertions are also available. - $this->assertCount(0, $response->toArray()['hydra:member']); + $this->assertCount(0, $response->toArray()['member']); } } } diff --git a/docs/guides/validate-incoming-data.php b/docs/guides/validate-incoming-data.php index 2b8bbed03b2..afefd71189a 100644 --- a/docs/guides/validate-incoming-data.php +++ b/docs/guides/validate-incoming-data.php @@ -139,8 +139,8 @@ public function testValidation(): void // { // "@context": "/contexts/ConstraintViolationList", // "@type": "ConstraintViolationList", - // "hydra:title": "An error occurred", - // "hydra:description": "properties: The product must have the minimal properties required (\"description\", \"price\")", + // "title": "An error occurred", + // "description": "properties: The product must have the minimal properties required (\"description\", \"price\")", // "violations": [ // { // "propertyPath": "properties", @@ -151,7 +151,7 @@ public function testValidation(): void // ``` $this->assertResponseStatusCodeSame(422); $this->assertJsonContains([ - 'hydra:description' => 'properties: The product must have the minimal properties required ("description", "price")', + 'description' => 'properties: The product must have the minimal properties required ("description", "price")', 'title' => 'An error occurred', 'violations' => [ ['propertyPath' => 'properties', 'message' => 'The product must have the minimal properties required ("description", "price")'], diff --git a/docs/pdg.config.yaml b/docs/pdg.config.yaml index 39bc4f435ca..1970c1e1b82 100644 --- a/docs/pdg.config.yaml +++ b/docs/pdg.config.yaml @@ -5,7 +5,7 @@ pdg: src: './guides' references: base_url: '/docs/reference' - exclude: ['*Factory.php', '*.tpl.php', 'deprecation.php'] + exclude: ['*Factory.php', '*.php.tpl', 'deprecation.php'] exclude_path: ['OpenApi/Tests', 'JsonSchema/Tests', 'RamseyUuid/Tests', 'Metadata/Tests', 'HttpCache/Tests', 'Elasticsearch/Tests', 'Doctrine/Common/Tests', 'GraphQl/Tests', 'Serializer/Tests'] namespace: 'ApiPlatform' output: 'dist/reference' diff --git a/docs/src/Kernel.php b/docs/src/Kernel.php index 69e14eacaa4..e8c439562dd 100644 --- a/docs/src/Kernel.php +++ b/docs/src/Kernel.php @@ -164,9 +164,15 @@ public function executeMigrations(string $direction = Direction::UP): void $em = $this->getContainer()->get('doctrine.orm.entity_manager'); $loader = new ExistingEntityManager($em); $dependencyFactory = DependencyFactory::fromEntityManager($confLoader, $loader); + $metadataStorage = $dependencyFactory->getMetadataStorage(); - $dependencyFactory->getMetadataStorage()->ensureInitialized(); - $executed = $dependencyFactory->getMetadataStorage()->getExecutedMigrations(); + try { + $metadataStorage->ensureInitialized(); + } catch (\Exception) { + // table exists + } + + $executed = $metadataStorage->getExecutedMigrations(); if ($executed->hasMigration(new Version($migrationClass)) && Direction::DOWN !== $direction) { continue; diff --git a/features/doctrine/issue6039/entity_class_option.feature b/features/doctrine/issue6039/entity_class_option.feature deleted file mode 100644 index 18609626a2e..00000000000 --- a/features/doctrine/issue6039/entity_class_option.feature +++ /dev/null @@ -1,12 +0,0 @@ -Feature: Test entity class option on collections - In order to retrieve a collections of resources mapped to a DTO automatically - As a client software developer - - @createSchema - @!mongodb - Scenario: Get collection - Given there are issue6039 users - And I add "Accept" header equal to "application/ld+json" - When I send a "GET" request to "/issue6039_user_apis" - Then the response status code should be 200 - And the JSON node "hydra:member[0].bar" should not exist diff --git a/features/doctrine/order_filter.feature b/features/doctrine/order_filter.feature index 285f8557906..4d3d5587b97 100644 --- a/features/doctrine/order_filter.feature +++ b/features/doctrine/order_filter.feature @@ -241,65 +241,6 @@ Feature: Order filter on collections } """ - Scenario: Get collection ordered by default configured order on a string property and on which order filter has been enabled in whitelist mode with default descending order - When I send a "GET" request to "/dummies?order[name]" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be valid according to this schema: - """ - { - "type": "object", - "properties": { - "@context": {"pattern": "^/contexts/Dummy$"}, - "@id": {"pattern": "^/dummies$"}, - "@type": {"pattern": "^hydra:Collection$"}, - "hydra:member": { - "type": "array", - "items": [ - { - "type": "object", - "properties": { - "@id": { - "type": "string", - "pattern": "^/dummies/9$" - } - } - }, - { - "type": "object", - "properties": { - "@id": { - "type": "string", - "pattern": "^/dummies/8$" - } - } - }, - { - "type": "object", - "properties": { - "@id": { - "type": "string", - "pattern": "^/dummies/7$" - } - } - } - ], - "additionalItems": false, - "maxItems": 3, - "minItems": 3 - }, - "hydra:view": { - "type": "object", - "properties": { - "@id": {"pattern": "^/dummies\\?order%5Bname%5D="}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } - } - } - } - """ - Scenario: Get collection ordered collection on several property keep the order # Adding 30 more data with the same name Given there are 30 dummy objects @@ -482,121 +423,7 @@ Feature: Order filter on collections Scenario: Get collection ordered by default configured order on a embedded string property and on which order filter has been enabled in whitelist mode with default descending order When I send a "GET" request to "/embedded_dummies?order[embeddedDummy.dummyName]" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be valid according to this schema: - """ - { - "type": "object", - "properties": { - "@context": {"pattern": "^/contexts/EmbeddedDummy$"}, - "@id": {"pattern": "^/embedded_dummies$"}, - "@type": {"pattern": "^hydra:Collection$"}, - "hydra:member": { - "type": "array", - "items": [ - { - "type": "object", - "properties": { - "@id": { - "type": "string", - "pattern": "^/embedded_dummies/9" - } - } - }, - { - "type": "object", - "properties": { - "@id": { - "type": "string", - "pattern": "^/embedded_dummies/8" - } - } - }, - { - "type": "object", - "properties": { - "@id": { - "type": "string", - "pattern": "^/embedded_dummies/7" - } - } - } - ], - "additionalItems": false, - "maxItems": 3, - "minItems": 3 - }, - "hydra:view": { - "type": "object", - "properties": { - "@id": {"pattern": "^/embedded_dummies\\?order%5BembeddedDummy\\.dummyName%5D="}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } - } - } - } - """ - - Scenario: Get a collection even if the order parameter is not well-formed - When I send a "GET" request to "/dummies?sort=id&order=asc" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be valid according to this schema: - """ - { - "type": "object", - "properties": { - "@context": {"pattern": "^/contexts/Dummy$"}, - "@id": {"pattern": "^/dummies$"}, - "@type": {"pattern": "^hydra:Collection$"}, - "hydra:member": { - "type": "array", - "items": [ - { - "type": "object", - "properties": { - "@id": { - "type": "string", - "pattern": "^/dummies/1$" - } - } - }, - { - "type": "object", - "properties": { - "@id": { - "type": "string", - "pattern": "^/dummies/2$" - } - } - }, - { - "type": "object", - "properties": { - "@id": { - "type": "string", - "pattern": "^/dummies/3$" - } - } - } - ], - "additionalItems": false, - "maxItems": 3, - "minItems": 3 - }, - "hydra:view": { - "type": "object", - "properties": { - "@id": {"pattern": "^/dummies"}, - "@type": {"pattern": "^hydra:PartialCollectionView$"} - } - } - } - } - """ + Then the response status code should be 422 Scenario: Get collection ordered by a non valid properties and on which order filter has been enabled in whitelist mode When I send a "GET" request to "/dummies?order[alias]=asc" diff --git a/features/filter/filter_validation.feature b/features/filter/filter_validation.feature index 69648bbb8ff..326c23982f8 100644 --- a/features/filter/filter_validation.feature +++ b/features/filter/filter_validation.feature @@ -10,13 +10,13 @@ Feature: Validate filters based upon filter description Scenario: Required filter that does not allow empty value should throw an error if empty When I am on "/filter_validators?required=&required-allow-empty=&arrayRequired[foo]=" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "required" does not allow empty value' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'required: This value should not be blank.' Scenario: Required filter should throw an error if not set When I am on "/filter_validators" - Then the response status code should be 400 - And the JSON node "detail" should match '/^Query parameter "required" is required\nQuery parameter "required-allow-empty" is required$/' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'required: This value should not be blank.\nrequired-allow-empty: The parameter "required-allow-empty" is required.' Scenario: Required filter should not throw an error if set When I am on "/array_filter_validators?arrayRequired[]=foo&indexedArrayRequired[foo]=foo" @@ -24,76 +24,64 @@ Feature: Validate filters based upon filter description Scenario: Required filter should throw an error if not set When I am on "/array_filter_validators" - Then the response status code should be 400 - And the JSON node "detail" should match '/^Query parameter "arrayRequired\[\]" is required\nQuery parameter "indexedArrayRequired\[foo\]" is required$/' - - When I am on "/array_filter_validators?arrayRequired=foo&indexedArrayRequired[foo]=foo" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "arrayRequired[]" is required' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'arrayRequired[]: This value should not be blank.\nindexedArrayRequired[foo]: This value should not be blank.' When I am on "/array_filter_validators?arrayRequired[foo]=foo" - Then the response status code should be 400 - And the JSON node "detail" should match '/^Query parameter "arrayRequired\[\]" is required\nQuery parameter "indexedArrayRequired\[foo\]" is required$/' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'indexedArrayRequired[foo]: This value should not be blank.' When I am on "/array_filter_validators?arrayRequired[]=foo" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "indexedArrayRequired[foo]" is required' - - When I am on "/array_filter_validators?arrayRequired[]=foo&indexedArrayRequired[bar]=bar" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "indexedArrayRequired[foo]" is required' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'indexedArrayRequired[foo]: This value should not be blank.' Scenario: Test filter bounds: maximum When I am on "/filter_validators?required=foo&required-allow-empty&maximum=10" Then the response status code should be 200 When I am on "/filter_validators?required=foo&required-allow-empty&maximum=11" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "maximum" must be less than or equal to 10' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'maximum: This value should be less than or equal to 10.' Scenario: Test filter bounds: exclusiveMaximum When I am on "/filter_validators?required=foo&required-allow-empty&exclusiveMaximum=9" Then the response status code should be 200 When I am on "/filter_validators?required=foo&required-allow-empty&exclusiveMaximum=10" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "exclusiveMaximum" must be less than 10' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'exclusiveMaximum: This value should be less than 10.' Scenario: Test filter bounds: minimum When I am on "/filter_validators?required=foo&required-allow-empty&minimum=5" Then the response status code should be 200 When I am on "/filter_validators?required=foo&required-allow-empty&minimum=0" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "minimum" must be greater than or equal to 5' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'minimum: This value should be greater than or equal to 5.' Scenario: Test filter bounds: exclusiveMinimum When I am on "/filter_validators?required=foo&required-allow-empty&exclusiveMinimum=6" Then the response status code should be 200 When I am on "/filter_validators?required=foo&required-allow-empty&exclusiveMinimum=5" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "exclusiveMinimum" must be greater than 5' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'exclusiveMinimum: This value should be greater than 5.' Scenario: Test filter bounds: max length When I am on "/filter_validators?required=foo&required-allow-empty&max-length-3=123" Then the response status code should be 200 When I am on "/filter_validators?required=foo&required-allow-empty&max-length-3=1234" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "max-length-3" length must be lower than or equal to 3' - - Scenario: Do not throw an error if value is not an array - When I am on "/filter_validators?required=foo&required-allow-empty&max-length-3[]=12345" - Then the response status code should be 200 + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'max-length-3: This value is too long. It should have 3 characters or less.' Scenario: Test filter bounds: min length When I am on "/filter_validators?required=foo&required-allow-empty&min-length-3=123" Then the response status code should be 200 When I am on "/filter_validators?required=foo&required-allow-empty&min-length-3=12" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "min-length-3" length must be greater than or equal to 3' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'min-length-3: This value is too short. It should have 3 characters or more.' Scenario: Test filter pattern When I am on "/filter_validators?required=foo&required-allow-empty&pattern=pattern" @@ -101,70 +89,21 @@ Feature: Validate filters based upon filter description Then the response status code should be 200 When I am on "/filter_validators?required=foo&required-allow-empty&pattern=not-pattern" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "pattern" must match pattern /^(pattern|nrettap)$/' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'pattern: This value is not valid.' Scenario: Test filter enum When I am on "/filter_validators?required=foo&required-allow-empty&enum=in-enum" Then the response status code should be 200 When I am on "/filter_validators?required=foo&required-allow-empty&enum=not-in-enum" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "enum" must be one of "in-enum, mune-ni"' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'enum: The value you selected is not a valid choice.' Scenario: Test filter multipleOf When I am on "/filter_validators?required=foo&required-allow-empty&multiple-of=4" Then the response status code should be 200 When I am on "/filter_validators?required=foo&required-allow-empty&multiple-of=3" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "multiple-of" must multiple of 2' - - Scenario: Test filter array items csv format minItems - When I am on "/filter_validators?required=foo&required-allow-empty&csv-min-2=a,b" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&csv-min-2=a" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "csv-min-2" must contain more than 2 values' - - Scenario: Test filter array items csv format maxItems - When I am on "/filter_validators?required=foo&required-allow-empty&csv-max-3=a,b,c" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&csv-max-3=a,b,c,d" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "csv-max-3" must contain less than 3 values' - - Scenario: Test filter array items tsv format minItems - When I am on "/filter_validators?required=foo&required-allow-empty&tsv-min-2=a\tb" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&tsv-min-2=a,b" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "tsv-min-2" must contain more than 2 values' - - Scenario: Test filter array items pipes format minItems - When I am on "/filter_validators?required=foo&required-allow-empty&pipes-min-2=a|b" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&pipes-min-2=a,b" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "pipes-min-2" must contain more than 2 values' - - Scenario: Test filter array items ssv format minItems - When I am on "/filter_validators?required=foo&required-allow-empty&ssv-min-2=a b" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&ssv-min-2=a,b" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "ssv-min-2" must contain more than 2 values' - - @dropSchema - Scenario: Test filter array items unique items - When I am on "/filter_validators?required=foo&required-allow-empty&csv-uniques=a,b" - Then the response status code should be 200 - - When I am on "/filter_validators?required=foo&required-allow-empty&csv-uniques=a,a" - Then the response status code should be 400 - And the JSON node "detail" should be equal to 'Query parameter "csv-uniques" must contain unique values' + Then the response status code should be 422 + And the JSON node "detail" should be equal to 'multiple-of: This value should be a multiple of 2.' diff --git a/features/hal/problem_legacy.feature b/features/hal/problem_legacy.feature deleted file mode 100644 index d2921884503..00000000000 --- a/features/hal/problem_legacy.feature +++ /dev/null @@ -1,50 +0,0 @@ -Feature: Error handling valid according to RFC 7807 (application/problem+json) - In order to be able to handle error client side - As a client software developer - I need to retrieve an RFC 7807 compliant serialization of errors - - Scenario: Get an error - When I add "Content-Type" header equal to "application/json" - And I add "Accept" header equal to "application/json" - And I send a "POST" request to "/dummies" with body: - """ - {} - """ - Then the response status code should be 422 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" - And the JSON should be equal to: - """ - { - "type": "https:\/\/tools.ietf.org\/html\/rfc2616#section-10", - "title": "An error occurred", - "detail": "name: This value should not be blank.", - "violations": [ - { - "propertyPath": "name", - "message": "This value should not be blank.", - "code": "c1051bb4-d103-4f74-8988-acbcafc7fdc3" - } - ] - } - """ - - Scenario: Get an error during deserialization of simple relation - When I add "Content-Type" header equal to "application/json" - And I add "Accept" header equal to "application/json" - And I send a "POST" request to "/dummies" with body: - """ - { - "name": "Foo", - "relatedDummy": { - "name": "bar" - } - } - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" - And the JSON node "type" should be equal to "https://tools.ietf.org/html/rfc2616#section-10" - And the JSON node "title" should be equal to "An error occurred" - And the JSON node "detail" should be equal to 'Nested documents for attribute "relatedDummy" are not allowed. Use IRIs instead.' - And the JSON node "trace" should exist diff --git a/features/http_cache/headers.feature b/features/http_cache/headers.feature index b14b412bdbd..2fe42bd14c3 100644 --- a/features/http_cache/headers.feature +++ b/features/http_cache/headers.feature @@ -7,6 +7,6 @@ Feature: Default values of HTTP cache headers Scenario: Cache headers default value When I send a "GET" request to "/relation_embedders" Then the response status code should be 200 - And the header "Etag" should be equal to '"7bfa587950d675e222660f68623f5f89"' + And the header "Etag" should be equal to '"032297ac74d75a50"' And the header "Cache-Control" should be equal to "max-age=60, public, s-maxage=3600" And the header "Vary" should be equal to "Accept, Cookie" diff --git a/features/http_cache/tag_collector_service.feature b/features/http_cache/tag_collector_service.feature index 864ac7ee4be..ed994aadb7e 100644 --- a/features/http_cache/tag_collector_service.feature +++ b/features/http_cache/tag_collector_service.feature @@ -17,7 +17,7 @@ Feature: Cache invalidation through HTTP Cache tags (custom TagCollector service Then the response status code should be 201 And the header "Cache-Tags" should not exist - Scenario: TagCollector can identify $object (IRI is overriden with custom logic) + Scenario: TagCollector can identify $object (IRI is overridden with custom logic) When I send a "GET" request to "/relation_embedders/1" Then the response status code should be 200 And the header "Cache-Tags" should be equal to "/RE/1#anotherRelated,/RE/1#related,/RE/1" @@ -126,7 +126,7 @@ Feature: Cache invalidation through HTTP Cache tags (custom TagCollector service Then the response status code should be 201 And the header "Cache-Tags" should not exist - Scenario: TagCollector can read propertyMetadata (tag is overriden with data from extraProperties) + Scenario: TagCollector can read propertyMetadata (tag is overridden with data from extraProperties) When I send a "GET" request to "/extra_properties_on_properties/1" Then the response status code should be 200 And the header "Cache-Tags" should be equal to "/extra_properties_on_properties/1#overrideRelationTag,/extra_properties_on_properties/1" diff --git a/features/hydra/collection.feature b/features/hydra/collection.feature index 01270c79044..8e9b0c33aba 100644 --- a/features/hydra/collection.feature +++ b/features/hydra/collection.feature @@ -479,7 +479,7 @@ Feature: Collections support When I send a "GET" request to "/dummies?itemsPerPage=0&page=2" Then the response status code should be 400 - And the JSON node "hydra:description" should be equal to "Page should not be greater than 1 if limit is equal to 0" + And the JSON node "detail" should be equal to "Page should not be greater than 1 if limit is equal to 0" Scenario: Cursor-based pagination with an empty collection When I send a "GET" request to "/so_manies" @@ -586,3 +586,11 @@ Feature: Collections support } } """ + + Scenario: Hydra collection without prefix + When I send a "GET" request to "/no_hydra_prefixes" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON node "totalItems" should exist + And the JSON node "member" should exist diff --git a/features/hydra/docs.feature b/features/hydra/docs.feature index d848ac55969..de6da5aae8d 100644 --- a/features/hydra/docs.feature +++ b/features/hydra/docs.feature @@ -13,22 +13,19 @@ Feature: Documentation support And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" # Context - And the JSON node "@context.@vocab" should be equal to "http://example.com/docs.jsonld#" - And the JSON node "@context.hydra" should be equal to "http://www.w3.org/ns/hydra/core#" - And the JSON node "@context.rdf" should be equal to "http://www.w3.org/1999/02/22-rdf-syntax-ns#" - And the JSON node "@context.rdfs" should be equal to "http://www.w3.org/2000/01/rdf-schema#" - And the JSON node "@context.xmls" should be equal to "http://www.w3.org/2001/XMLSchema#" - And the JSON node "@context.owl" should be equal to "http://www.w3.org/2002/07/owl#" - And the JSON node "@context.domain.@id" should be equal to "rdfs:domain" - And the JSON node "@context.domain.@type" should be equal to "@id" - And the JSON node "@context.range.@id" should be equal to "rdfs:range" - And the JSON node "@context.range.@type" should be equal to "@id" - And the JSON node "@context.subClassOf.@id" should be equal to "rdfs:subClassOf" - And the JSON node "@context.subClassOf.@type" should be equal to "@id" - And the JSON node "@context.expects.@id" should be equal to "hydra:expects" - And the JSON node "@context.expects.@type" should be equal to "@id" - And the JSON node "@context.returns.@id" should be equal to "hydra:returns" - And the JSON node "@context.returns.@type" should be equal to "@id" + And the Hydra context matches the online resource "http://www.w3.org/ns/hydra/context.jsonld" + And the JSON node "@context[1].@vocab" should be equal to "http://example.com/docs.jsonld#" + And the JSON node "@context[1].hydra" should be equal to "http://www.w3.org/ns/hydra/core#" + And the JSON node "@context[1].rdf" should be equal to "http://www.w3.org/1999/02/22-rdf-syntax-ns#" + And the JSON node "@context[1].rdfs" should be equal to "http://www.w3.org/2000/01/rdf-schema#" + And the JSON node "@context[1].xmls" should be equal to "http://www.w3.org/2001/XMLSchema#" + And the JSON node "@context[1].owl" should be equal to "http://www.w3.org/2002/07/owl#" + And the JSON node "@context[1].domain.@id" should be equal to "rdfs:domain" + And the JSON node "@context[1].domain.@type" should be equal to "@id" + And the JSON node "@context[1].range.@id" should be equal to "rdfs:range" + And the JSON node "@context[1].range.@type" should be equal to "@id" + And the JSON node "@context[1].subClassOf.@id" should be equal to "rdfs:subClassOf" + And the JSON node "@context[1].subClassOf.@type" should be equal to "@id" # Root properties And the JSON node "@id" should be equal to "/docs.jsonld" And the JSON node "hydra:title" should be equal to "My Dummy API" @@ -36,9 +33,9 @@ Feature: Documentation support And the JSON node "hydra:description" should contain "Made with love" And the JSON node "hydra:entrypoint" should be equal to "/" # Supported classes - And the Hydra class "The API entrypoint" exists - And the Hydra class "A constraint violation" exists - And the Hydra class "A constraint violation list" exists + And the Hydra class "Entrypoint" exists + And the Hydra class "ConstraintViolation" exists + And the Hydra class "ConstraintViolationList" exists And the Hydra class "CircularReference" exists And the Hydra class "CustomIdentifierDummy" exists And the Hydra class "CustomNormalizedDummy" exists @@ -52,7 +49,6 @@ Feature: Documentation support # Doc And the value of the node "@id" of the Hydra class "Dummy" is "#Dummy" And the value of the node "@type" of the Hydra class "Dummy" is "hydra:Class" - And the value of the node "rdfs:label" of the Hydra class "Dummy" is "Dummy" And the value of the node "hydra:title" of the Hydra class "Dummy" is "Dummy" And the value of the node "hydra:description" of the Hydra class "Dummy" is "Dummy." # Properties @@ -65,7 +61,7 @@ Feature: Documentation support And the value of the node "@type" of the property "name" of the Hydra class "Dummy" is "hydra:SupportedProperty" And the value of the node "hydra:property.@id" of the property "name" of the Hydra class "Dummy" is "https://schema.org/name" And the value of the node "hydra:property.@type" of the property "name" of the Hydra class "Dummy" is "rdf:Property" - And the value of the node "hydra:property.rdfs:label" of the property "name" of the Hydra class "Dummy" is "name" + And the value of the node "hydra:property.label" of the property "name" of the Hydra class "Dummy" is "name" And the value of the node "hydra:property.domain" of the property "name" of the Hydra class "Dummy" is "#Dummy" And the value of the node "hydra:property.range" of the property "name" of the Hydra class "Dummy" is "xmls:string" And the value of the node "hydra:property.range" of the property "relatedDummy" of the Hydra class "Dummy" is "https://schema.org/Product" @@ -77,14 +73,16 @@ Feature: Documentation support And the value of the node "@type" of the operation "GET" of the Hydra class "Dummy" contains "hydra:Operation" And the value of the node "@type" of the operation "GET" of the Hydra class "Dummy" contains "schema:FindAction" And the value of the node "hydra:method" of the operation "GET" of the Hydra class "Dummy" is "GET" - And the value of the node "hydra:title" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource." - And the value of the node "rdfs:label" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource." - And the value of the node "returns" of the operation "GET" of the Hydra class "Dummy" is "#Dummy" - And the value of the node "hydra:title" of the operation "PUT" of the Hydra class "Dummy" is "Replaces the Dummy resource." - And the value of the node "hydra:title" of the operation "DELETE" of the Hydra class "Dummy" is "Deletes the Dummy resource." + And the value of the node "hydra:title" of the operation "GET" of the Hydra class "Dummy" is "getDummy" + And the value of the node "hydra:description" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource." + And the value of the node "returns" of the operation "GET" of the Hydra class "Dummy" is "Dummy" + And the value of the node "hydra:title" of the operation "PUT" of the Hydra class "Dummy" is "putDummy" + And the value of the node "hydra:description" of the operation "PUT" of the Hydra class "Dummy" is "Replaces the Dummy resource." + And the value of the node "hydra:description" of the operation "DELETE" of the Hydra class "Dummy" is "Deletes the Dummy resource." + And the value of the node "hydra:title" of the operation "DELETE" of the Hydra class "Dummy" is "deleteDummy" And the value of the node "returns" of the operation "DELETE" of the Hydra class "Dummy" is "owl:Nothing" # Deprecations And the boolean value of the node "owl:deprecated" of the Hydra class "DeprecatedResource" is true And the boolean value of the node "hydra:property.owl:deprecated" of the property "deprecatedField" of the Hydra class "DeprecatedResource" is true - And the boolean value of the node "owl:deprecated" of the property "The collection of DeprecatedResource resources" of the Hydra class "The API entrypoint" is true + And the boolean value of the node "owl:deprecated" of the property "getDeprecatedResourceCollection" of the Hydra class "Entrypoint" is true And the boolean value of the node "owl:deprecated" of the operation "GET" of the Hydra class "DeprecatedResource" is true diff --git a/features/hydra/error.feature b/features/hydra/error.feature index 50e1c869b3e..07fe8210f02 100644 --- a/features/hydra/error.feature +++ b/features/hydra/error.feature @@ -16,13 +16,14 @@ Feature: Error handling And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' And the JSON node "type" should exist - And the JSON node "title" should be equal to "An error occurred" + And the JSON node "title" should not exists And the JSON node "hydra:title" should be equal to "An error occurred" And the JSON node "detail" should exist + And the JSON node "description" should not exist And the JSON node "hydra:description" should exist And the JSON node "trace" should exist And the JSON node "status" should exist - And the JSON node "@context" should not exist + And the JSON node "@context" should exist Scenario: Get validation constraint violations When I add "Content-Type" header equal to "application/ld+json" @@ -36,8 +37,9 @@ Feature: Error handling And the JSON should be equal to: """ { + "@context": "/contexts/ConstraintViolation", "@id": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3", - "@type": "ConstraintViolationList", + "@type": "ConstraintViolation", "status": 422, "violations": [ { @@ -49,8 +51,7 @@ Feature: Error handling "detail": "name: This value should not be blank.", "hydra:title": "An error occurred", "hydra:description": "name: This value should not be blank.", - "type": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3", - "title": "An error occurred" + "type": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3" } """ @@ -64,9 +65,9 @@ Feature: Error handling And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' - And the JSON node "@context" should not exist + And the JSON node "@context" should exist And the JSON node "type" should exist - And the JSON node "title" should be equal to "An error occurred" + And the JSON node "hydra:title" should be equal to "An error occurred" And the JSON node "detail" should exist Scenario: Get an rfc 7807 not found error @@ -79,12 +80,11 @@ Feature: Error handling And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' - And the JSON node "@context" should not exist + And the JSON node "@context" should exist And the JSON node "type" should exist - And the JSON node "title" should be equal to "An error occurred" And the JSON node "hydra:title" should be equal to "An error occurred" And the JSON node "detail" should exist - And the JSON node "hydra:description" should exist + And the JSON node "description" should not exist Scenario: Get an rfc 7807 bad method error When I add "Content-Type" header equal to "application/ld+json" @@ -97,12 +97,11 @@ Feature: Error handling And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' - And the JSON node "@context" should not exist + And the JSON node "@context" should exist And the JSON node "type" should exist - And the JSON node "title" should be equal to "An error occurred" And the JSON node "hydra:title" should be equal to "An error occurred" And the JSON node "detail" should exist - And the JSON node "hydra:description" should exist + And the JSON node "description" should not exist Scenario: Get an rfc 7807 validation error When I add "Content-Type" header equal to "application/ld+json" @@ -115,8 +114,25 @@ Feature: Error handling And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' - And the JSON node "@context" should not exist + And the JSON node "@context" should exist And the JSON node "type" should exist - And the JSON node "title" should be equal to "An error occurred" + And the JSON node "hydra:title" should be equal to "An error occurred" And the JSON node "detail" should exist And the JSON node "violations" should exist + + Scenario: Get an rfc 7807 error + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/exception_problems_without_prefix" with body: + """ + {} + """ + Then the response status code should be 400 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" + And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' + And the JSON node "type" should exist + And the JSON node "hydra:title" should be equal to "An error occurred" + And the JSON node "detail" should exist + And the JSON node "description" should not exist + And the JSON node "trace" should exist + And the JSON node "status" should exist diff --git a/features/hydra/error_legacy.feature b/features/hydra/error_legacy.feature deleted file mode 100644 index 9b97476e315..00000000000 --- a/features/hydra/error_legacy.feature +++ /dev/null @@ -1,165 +0,0 @@ -Feature: Error handling - In order to be able to handle error client side - As a client software developer - I need to retrieve an Hydra serialization of errors - - Scenario: Get an error - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - {} - """ - Then the response status code should be 422 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be equal to: - """ - { - "@context": "/contexts/ConstraintViolationList", - "@type": "ConstraintViolationList", - "hydra:title": "An error occurred", - "hydra:description": "name: This value should not be blank.", - "violations": [ - { - "propertyPath": "name", - "message": "This value should not be blank.", - "code": "c1051bb4-d103-4f74-8988-acbcafc7fdc3" - } - ] - } - """ - - Scenario: Get an error during deserialization of simple relation - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - { - "name": "Foo", - "relatedDummy": { - "name": "bar" - } - } - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should be equal to "/contexts/Error" - And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'Nested documents for attribute "relatedDummy" are not allowed. Use IRIs instead.' - And the JSON node "trace" should exist - - Scenario: Get an error during deserialization of collection - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - { - "name": "Foo", - "relatedDummies": [{ - "name": "bar" - }] - } - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should be equal to "/contexts/Error" - And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'Nested documents for attribute "relatedDummies" are not allowed. Use IRIs instead.' - And the JSON node "trace" should exist - - Scenario: Get an error because of an invalid JSON - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - { - "name": "Foo", - } - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should be equal to "/contexts/Error" - And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should exist - And the JSON node "trace" should exist - - Scenario: Get an error during update of an existing resource with a non-allowed update operation - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/dummies" with body: - """ - { - "@id": "/dummies/1", - "name": "Foo" - } - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should be equal to "/contexts/Error" - And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to "Update is not allowed for this operation." - And the JSON node "trace" should exist - - @createSchema - Scenario: Populate database with related dummies. Check that id will be "/related_dummies/1" - Given I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/related_dummies" with body: - """ - { - "@type": "https://schema.org/Product", - "symfony": "laravel" - } - """ - Then the response status code should be 201 - And the response should be in JSON - And the JSON node "@id" should be equal to "/related_dummies/1" - And the JSON node "symfony" should be equal to "laravel" - - Scenario: Do not get an error during update of an existing relation with a non-allowed update operation - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/relation_embedders" with body: - """ - { - "anotherRelated": { - "@id": "/related_dummies/1", - "@type": "https://schema.org/Product", - "symfony": "phalcon" - } - } - """ - Then the response status code should be 201 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should be equal to "/contexts/RelationEmbedder" - And the JSON node "@type" should be equal to "RelationEmbedder" - And the JSON node "@id" should be equal to "/relation_embedders/1" - And the JSON node "anotherRelated.@id" should be equal to "/related_dummies/1" - And the JSON node "anotherRelated.symfony" should be equal to "phalcon" - - Scenario: Get an error because of sending bad type property - When I add "Content-Type" header equal to "application/json" - And I add "Accept" header equal to "application/ld+json" - And I send a "POST" request to "/greetings" with body: - """ - { - "0": 1 - } - """ - Then the response status code should be 201 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - - Scenario: Get an rfc 7807 error with backward compatibility - When I add "Content-Type" header equal to "application/ld+json" - And I send a "POST" request to "/exception_problems_with_compatibility" with body: - """ - {} - """ - Then the response status code should be 400 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "@context" should exist diff --git a/features/json/input_output.feature b/features/json/input_output.feature index 5f1982c0098..2a5a1143162 100644 --- a/features/json/input_output.feature +++ b/features/json/input_output.feature @@ -9,7 +9,7 @@ Feature: JSON DTO input and output @createSchema Scenario: Request a password reset - And I send a "POST" request to "/users/password_reset_request" with body: + And I send a "POST" request to "/users_reset/password_reset_request" with body: """ { "email": "user@example.com" @@ -27,7 +27,7 @@ Feature: JSON DTO input and output @createSchema Scenario: Request a password reset for a non-existent user - And I send a "POST" request to "/users/password_reset_request" with body: + And I send a "POST" request to "/users_reset/password_reset_request" with body: """ { "email": "does-not-exist@example.com" diff --git a/features/jsonapi/errors_legacy.feature b/features/jsonapi/errors_legacy.feature deleted file mode 100644 index 587ee74a837..00000000000 --- a/features/jsonapi/errors_legacy.feature +++ /dev/null @@ -1,73 +0,0 @@ -Feature: JSON API error handling - In order to be able to handle error client side - As a client software developer - I need to retrieve an JSON API serialization of errors - - Background: - Given I add "Accept" header equal to "application/vnd.api+json" - And I add "Content-Type" header equal to "application/vnd.api+json" - - @createSchema - Scenario: Get a validation error on an attribute - When I send a "POST" request to "/dummies" with body: - """ - { - "data": { - "type": "dummy", - "attributes": {} - } - } - """ - Then the response status code should be 422 - And the response should be in JSON - And the JSON should be valid according to the JSON API schema - And the JSON should be equal to: - """ - { - "errors": [ - { - "detail": "This value should not be blank.", - "source": { - "pointer": "data/attributes/name" - } - } - ] - } - """ - - Scenario: Get a validation error on an relationship - Given there is a RelatedDummy - And there is a DummyFriend - When I send a "POST" request to "/related_to_dummy_friends" with body: - """ - { - "data": { - "type": "RelatedToDummyFriend", - "attributes": { - "name": "Related to dummy friend" - } - } - } - """ - Then the response status code should be 422 - And the response should be in JSON - And the JSON should be valid according to the JSON API schema - And the JSON should be equal to: - """ - { - "errors": [ - { - "detail": "This value should not be null.", - "source": { - "pointer": "data/relationships/dummyFriend" - } - }, - { - "detail": "This value should not be null.", - "source": { - "pointer": "data/relationships/relatedDummy" - } - } - ] - } - """ diff --git a/features/jsonapi/related-resouces-inclusion.feature b/features/jsonapi/related-resouces-inclusion.feature index ba171bece72..2a6663d3fd1 100644 --- a/features/jsonapi/related-resouces-inclusion.feature +++ b/features/jsonapi/related-resouces-inclusion.feature @@ -54,7 +54,9 @@ Feature: JSON API Inclusion of Related Resources } """ + @createSchema Scenario: Request inclusion of a non existing related resource + Given there are 3 dummy property objects When I send a "GET" request to "/dummy_properties/1?include=foo" Then the response status code should be 200 And the response should be in JSON @@ -87,7 +89,9 @@ Feature: JSON API Inclusion of Related Resources } """ + @createSchema Scenario: Request inclusion of a related resource keeping main object properties unfiltered + Given there are 3 dummy property objects When I send a "GET" request to "/dummy_properties/1?include=group&fields[group]=id,foo&fields[DummyProperty]=bar,baz" Then the response status code should be 200 And the response should be in JSON diff --git a/features/jsonld/input_output.feature b/features/jsonld/input_output.feature index 73b1ba6e34c..3840877de05 100644 --- a/features/jsonld/input_output.feature +++ b/features/jsonld/input_output.feature @@ -309,7 +309,7 @@ Feature: JSON-LD DTO input and output """ Then the response status code should be 400 And the response should be in JSON - And the JSON node "hydra:description" should be equal to "The input data is misformatted." + And the JSON node "detail" should be equal to "The input data is misformatted." @!mongodb Scenario: Reset password through an input DTO without DataTransformer diff --git a/features/jsonld/interface_as_resource.feature b/features/jsonld/interface_as_resource.feature index 00e0224b149..7236ace7ae7 100644 --- a/features/jsonld/interface_as_resource.feature +++ b/features/jsonld/interface_as_resource.feature @@ -15,7 +15,7 @@ Feature: JSON-LD using interface as resource "code": "WONDERFUL_TAXON" } """ - When I send a "GET" request to "/taxons/WONDERFUL_TAXON" + When I send a "GET" request to "/taxa/WONDERFUL_TAXON" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" @@ -23,7 +23,7 @@ Feature: JSON-LD using interface as resource """ { "@context": "/contexts/Taxon", - "@id": "/taxons/WONDERFUL_TAXON", + "@id": "/taxa/WONDERFUL_TAXON", "@type": "Taxon", "code": "WONDERFUL_TAXON" } @@ -49,7 +49,7 @@ Feature: JSON-LD using interface as resource "@type": "Product", "code": "GREAT_PRODUCT", "mainTaxon": { - "@id": "/taxons/WONDERFUL_TAXON", + "@id": "/taxa/WONDERFUL_TAXON", "@type": "Taxon", "code": "WONDERFUL_TAXON" } diff --git a/features/main/attribute_resource.feature b/features/main/attribute_resource.feature index 90a8bff1663..da92073e98f 100644 --- a/features/main/attribute_resource.feature +++ b/features/main/attribute_resource.feature @@ -100,7 +100,7 @@ Feature: Resource attributes And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "Link" should contain '; rel="http://www.w3.org/ns/json-ld#error"' - And the JSON node "hydra:description" should be equal to 'Unable to generate an IRI for the item of type "ApiPlatform\Tests\Fixtures\TestBundle\Entity\IncompleteUriVariableConfigured"' + And the JSON node "detail" should be equal to 'Unable to generate an IRI for the item of type "ApiPlatform\Tests\Fixtures\TestBundle\Entity\IncompleteUriVariableConfigured"' Scenario: Uri variables with Post operation When I add "Content-Type" header equal to "application/ld+json" diff --git a/features/main/crud.feature b/features/main/crud.feature index 1fc310d63bb..5933812bccc 100644 --- a/features/main/crud.feature +++ b/features/main/crud.feature @@ -22,7 +22,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/dummies/1" + And the header "Content-Location" should be equal to "/dummies/1.jsonld" And the header "Location" should be equal to "/dummies/1" And the JSON should be equal to: """ @@ -60,6 +60,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -95,17 +96,19 @@ Feature: Create-Retrieve-Update-Delete When I add "Content-Type" header equal to "application/ld+json" And I send a "POST" request to "/dummies" Then the response status code should be 400 - And the JSON node "hydra:description" should be equal to "Syntax error" + And the JSON node "detail" should be equal to "Syntax error" Scenario: Get a not found exception When I send a "GET" request to "/dummies/42" Then the response status code should be 404 + And the header "Content-Location" should not exist Scenario: Get a collection When I send a "GET" request to "/dummies" Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -513,7 +516,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/dummies/1" + And the header "Content-Location" should be equal to "/dummies/1.jsonld" And the JSON should be equal to: """ { @@ -551,7 +554,7 @@ Feature: Create-Retrieve-Update-Delete When I add "Content-Type" header equal to "application/ld+json" And I send a "PUT" request to "/dummies/1" Then the response status code should be 400 - And the JSON node "hydra:description" should be equal to "Syntax error" + And the JSON node "detail" should be equal to "Syntax error" Scenario: Delete a resource When I send a "DELETE" request to "/dummies/1" @@ -571,7 +574,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/processor_entities/1" + And the header "Content-Location" should be equal to "/processor_entities/1.jsonld" And the header "Location" should be equal to "/processor_entities/1" And the JSON should be equal to: """ @@ -596,7 +599,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/provider_entities/1" + And the header "Content-Location" should be equal to "/provider_entities/1.jsonld" And the header "Location" should be equal to "/provider_entities/1" And the JSON should be equal to: """ @@ -615,6 +618,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -639,6 +643,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -656,6 +661,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -675,6 +681,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -717,6 +724,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -736,6 +744,7 @@ Feature: Create-Retrieve-Update-Delete Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { diff --git a/features/main/crud_abstract.feature b/features/main/crud_abstract.feature index 87d6ca2578f..fc8bd66c69a 100644 --- a/features/main/crud_abstract.feature +++ b/features/main/crud_abstract.feature @@ -16,7 +16,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the header "Content-Location" should be equal to "/concrete_dummies/1.jsonld" And the header "Location" should be equal to "/concrete_dummies/1" And the JSON should be equal to: """ @@ -35,6 +35,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -52,6 +53,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be valid according to this schema: """ { @@ -92,7 +94,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the header "Content-Location" should be equal to "/concrete_dummies/1.jsonld" And the JSON should be equal to: """ { @@ -118,7 +120,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the header "Content-Location" should be equal to "/concrete_dummies/1.jsonld" And the JSON should be equal to: """ { @@ -150,7 +152,7 @@ Feature: Create-Retrieve-Update-Delete on abstract resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/concrete_dummies/1" + And the header "Content-Location" should be equal to "/concrete_dummies/1.jsonld" And the header "Location" should be equal to "/concrete_dummies/1" And the JSON should be equal to: """ diff --git a/features/main/crud_uri_variables.feature b/features/main/crud_uri_variables.feature index fb86d203961..37787dc56d2 100644 --- a/features/main/crud_uri_variables.feature +++ b/features/main/crud_uri_variables.feature @@ -13,7 +13,7 @@ Feature: Uri Variables Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/companies/1" + And the header "Content-Location" should be equal to "/companies/1.jsonld" And the header "Location" should be equal to "/companies/1" And the JSON should be equal to: """ @@ -51,7 +51,7 @@ Feature: Uri Variables Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/companies/1/employees/1" + And the header "Content-Location" should be equal to "/companies/1/employees/1.jsonld" And the header "Location" should be equal to "/companies/1/employees/1" And the JSON should be equal to: """ @@ -96,6 +96,7 @@ Feature: Uri Variables Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -164,6 +165,7 @@ Feature: Uri Variables Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -183,6 +185,7 @@ Feature: Uri Variables Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Location" should not exist And the JSON should be equal to: """ { @@ -200,3 +203,4 @@ Feature: Uri Variables When I add "Content-Type" header equal to "application/ld+json" And I send a "GET" request to "/companies/1/employees/2" Then the response status code should be 404 + And the header "Content-Location" should not exist diff --git a/features/main/custom_normalized.feature b/features/main/custom_normalized.feature index 67a668fa0f0..7d1a49f3fed 100644 --- a/features/main/custom_normalized.feature +++ b/features/main/custom_normalized.feature @@ -16,7 +16,7 @@ Feature: Using custom normalized entity Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_normalized_dummies/1" + And the header "Content-Location" should be equal to "/custom_normalized_dummies/1.jsonld" And the header "Location" should be equal to "/custom_normalized_dummies/1" And the JSON should be equal to: """ @@ -43,7 +43,7 @@ Feature: Using custom normalized entity Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the header "Content-Location" should be equal to "/related_normalized_dummies/1" + And the header "Content-Location" should be equal to "/related_normalized_dummies/1.json" And the header "Location" should be equal to "/related_normalized_dummies/1" And the JSON should be equal to: """ @@ -92,7 +92,7 @@ Feature: Using custom normalized entity Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the header "Content-Location" should be equal to "/related_normalized_dummies/1" + And the header "Content-Location" should be equal to "/related_normalized_dummies/1.json" And the JSON should be equal to: """ { @@ -158,7 +158,7 @@ Feature: Using custom normalized entity Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_normalized_dummies/1" + And the header "Content-Location" should be equal to "/custom_normalized_dummies/1.jsonld" And the JSON should be equal to: """ { @@ -182,7 +182,7 @@ Feature: Using custom normalized entity Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_normalized_dummies/1" + And the header "Content-Location" should be equal to "/custom_normalized_dummies/1.jsonld" And the JSON should be equal to: """ { diff --git a/features/main/custom_writable_identifier.feature b/features/main/custom_writable_identifier.feature index d2878f0afc5..097d253b9ad 100644 --- a/features/main/custom_writable_identifier.feature +++ b/features/main/custom_writable_identifier.feature @@ -16,7 +16,7 @@ Feature: Using custom writable identifier on resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/my_slug" + And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/my_slug.jsonld" And the header "Location" should be equal to "/custom_writable_identifier_dummies/my_slug" And the JSON should be equal to: """ @@ -81,7 +81,7 @@ Feature: Using custom writable identifier on resource Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/slug_modified" + And the header "Content-Location" should be equal to "/custom_writable_identifier_dummies/slug_modified.jsonld" And the JSON should be equal to: """ { diff --git a/features/main/not_exposed.feature b/features/main/not_exposed.feature index aa6cb252798..809b95dd8e5 100644 --- a/features/main/not_exposed.feature +++ b/features/main/not_exposed.feature @@ -171,13 +171,22 @@ Feature: Expose only a collection of objects When I send a "GET" request to "" Then the response status code should be 404 And the response should be in JSON - And the JSON node "hydra:description" should be equal to "" + And the JSON node "detail" should be equal to "" Examples: - | uri | hydra:description | - | /.well-known/genid/12345 | This route is not exposed on purpose. It generates an IRI for a collection resource without identifier nor item operation. | + | uri | description | | /tables/12345 | This route does not aim to be called. | | /forks/12345 | This route does not aim to be called. | + Scenario Outline: Get a not exposed route returns a 404 with an explanation + When I send a "GET" request to "" + Then the response status code should be 404 + And the response should be in JSON + And the JSON node "detail" should be equal to "" + Examples: + | uri | description | + | /.well-known/genid/12345 | This route is not exposed on purpose. It generates an IRI for a collection resource without identifier nor item operation. | + + Scenario: Get a single item still works When I send a "GET" request to "/cuillers/12345" Then the response status code should be 200 diff --git a/features/main/operation_resource.feature b/features/main/operation_resource.feature index 070cb5be9fd..b4bd729fcaf 100644 --- a/features/main/operation_resource.feature +++ b/features/main/operation_resource.feature @@ -53,7 +53,7 @@ Feature: Resource operations Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/operation_resources/1" + And the header "Content-Location" should be equal to "/operation_resources/1.jsonld" And the JSON should be equal to: """ { diff --git a/features/main/relation.feature b/features/main/relation.feature index 07d5050cda6..5eba540f96e 100644 --- a/features/main/relation.feature +++ b/features/main/relation.feature @@ -472,7 +472,7 @@ Feature: Relations support Then the response status code should be 400 And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" - And the JSON node "hydra:description" should contain 'Invalid IRI "certainly not an IRI".' + And the JSON node "detail" should contain 'Invalid IRI "certainly not an IRI".' Scenario: Passing an invalid type to a relation When I add "Content-Type" header equal to "application/ld+json" @@ -499,14 +499,14 @@ Feature: Relations support "type": "string", "pattern": "^An error occurred$" }, - "hydra:description": { + "detail": { "pattern": "^The type of the \"ApiPlatform\\\\Tests\\\\Fixtures\\\\TestBundle\\\\(Document|Entity)\\\\RelatedDummy\" resource must be \"array\" \\(nested document\\) or \"string\" \\(IRI\\), \"integer\" given.$" } }, "required": [ "@type", "hydra:title", - "hydra:description" + "detail" ] } """ diff --git a/features/main/union_intersect_types.feature b/features/main/union_intersect_types.feature index 97e415f60ea..73195804d38 100644 --- a/features/main/union_intersect_types.feature +++ b/features/main/union_intersect_types.feature @@ -118,4 +118,4 @@ Feature: Union/Intersect types Then the response status code should be 400 And the response should be in JSON And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" - And the JSON node "hydra:description" should be equal to 'Could not denormalize object of type "ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\ActivableInterface", no supporting normalizer found.' + And the JSON node "detail" should be equal to 'Could not denormalize object of type "ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\ActivableInterface", no supporting normalizer found.' diff --git a/features/main/uuid.feature b/features/main/uuid.feature index d0634b0bc94..a7506a15bec 100644 --- a/features/main/uuid.feature +++ b/features/main/uuid.feature @@ -16,7 +16,7 @@ Feature: Using uuid identifier on resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78" + And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78.jsonld" And the header "Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78" Scenario: Get a resource @@ -69,7 +69,7 @@ Feature: Using uuid identifier on resource Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78" + And the header "Content-Location" should be equal to "/uuid_identifier_dummies/41b29566-144b-11e6-a148-3e1d05defe78.jsonld" And the JSON should be equal to: """ { @@ -90,7 +90,7 @@ Feature: Using uuid identifier on resource Then the response status code should be 201 And the response should be in JSON And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the header "Content-Location" should be equal to "/custom_generated_identifiers/foo" + And the header "Content-Location" should be equal to "/custom_generated_identifiers/foo.jsonld" And the header "Location" should be equal to "/custom_generated_identifiers/foo" And the JSON should be equal to: """ diff --git a/features/main/validation.feature b/features/main/validation.feature index b8ab1feb8a6..40e22bdfb3a 100644 --- a/features/main/validation.feature +++ b/features/main/validation.feature @@ -26,13 +26,12 @@ Feature: Using validations groups """ Then the response status code should be 422 And the response should be in JSON - And the JSON should be equal to: + And the JSON should be a superset of: """ { - "@context": "/contexts/ConstraintViolationList", - "@type": "ConstraintViolationList", - "hydra:title": "An error occurred", - "hydra:description": "name: This value should not be null.", + "@context": "/contexts/ConstraintViolation", + "@type": "ConstraintViolation", + "detail": "name: This value should not be null.", "violations": [ { "propertyPath": "name", @@ -42,7 +41,7 @@ Feature: Using validations groups ] } """ - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" @createSchema Scenario: Create a resource with validation group sequence @@ -55,13 +54,12 @@ Feature: Using validations groups """ Then the response status code should be 422 And the response should be in JSON - And the JSON should be equal to: + And the JSON should be a superset of: """ { - "@context": "/contexts/ConstraintViolationList", - "@type": "ConstraintViolationList", - "hydra:title": "An error occurred", - "hydra:description": "title: This value should not be null.", + "@context": "/contexts/ConstraintViolation", + "@type": "ConstraintViolation", + "detail": "title: This value should not be null.", "violations": [ { "propertyPath": "title", @@ -71,7 +69,7 @@ Feature: Using validations groups ] } """ - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" @createSchema Scenario: Create a resource with serializedName property @@ -87,77 +85,9 @@ Feature: Using validations groups And the JSON node "violations[0].message" should be equal to "This value should not be null." And the JSON node "violations[0].propertyPath" should be equal to "test" And the JSON node "detail" should be equal to "test: This value should not be null." - And the JSON node "hydra:description" should be equal to "test: This value should not be null." And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" - @!mongodb @createSchema - Scenario: Create a resource with collectDenormalizationErrors - When I add "Content-type" header equal to "application/ld+json" - And I send a "POST" request to "/dummy_collect_denormalization" with body: - """ - { - "foo": 3, - "bar": "baz", - "qux": true, - "uuid": "y", - "relatedDummy": 8, - "relatedDummies": 76 - } - """ - Then the response status code should be 422 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON should be equal to: - """ - { - "@context": "/contexts/ConstraintViolationList", - "@type": "ConstraintViolationList", - "hydra:title": "An error occurred", - "hydra:description": "This value should be of type unknown.\nqux: This value should be of type string.\nfoo: This value should be of type bool.\nbar: This value should be of type int.\nuuid: This value should be of type uuid.\nrelatedDummy: This value should be of type array|string.\nrelatedDummies: This value should be of type array.", - "violations": [ - { - "propertyPath": "", - "message": "This value should be of type unknown.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40", - "hint": "Failed to create object because the class misses the \"baz\" property." - }, - { - "propertyPath": "qux", - "message": "This value should be of type string.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40" - }, - { - "propertyPath": "foo", - "message": "This value should be of type bool.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40" - }, - { - "propertyPath": "bar", - "message": "This value should be of type int.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40" - }, - { - "propertyPath": "uuid", - "message": "This value should be of type uuid.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40", - "hint": "Invalid UUID string: y" - }, - { - "propertyPath": "relatedDummy", - "message": "This value should be of type array|string.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40", - "hint": "The type of the \"relatedDummy\" attribute must be \"array\" (nested document) or \"string\" (IRI), \"integer\" given." - }, - { - "propertyPath": "relatedDummies", - "message": "This value should be of type array.", - "code": "ba785a8c-82cb-4283-967c-3cf342181b40" - } - ] - } - """ - @!mongodb Scenario: Get violations constraints When I add "Accept" header equal to "application/json" diff --git a/features/mongodb/filters.feature b/features/mongodb/filters.feature index 9e89239215d..5bb7c3b07e8 100644 --- a/features/mongodb/filters.feature +++ b/features/mongodb/filters.feature @@ -10,20 +10,18 @@ Feature: Filters on collections When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.badFourthLevel.level=4" Then the response status code should be 500 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to "Cannot use reference 'badFourthLevel' in class 'ThirdLevel' for lookup or graphLookup: dbRef references are not supported." + And the JSON node "detail" should be equal to "Cannot use reference 'badFourthLevel' in class 'ThirdLevel' for lookup or graphLookup: dbRef references are not supported." And the JSON node "trace" should exist Scenario: Error when getting collection with nested properties if references are not correctly stored (not owning side) When I send a "GET" request to "/dummies?relatedDummy.thirdLevel.fourthLevel.badThirdLevel.level=3" Then the response status code should be 500 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to "Cannot use reference 'badThirdLevel' in class 'FourthLevel' for lookup or graphLookup: dbRef references are not supported." + And the JSON node "detail" should be equal to "Cannot use reference 'badThirdLevel' in class 'FourthLevel' for lookup or graphLookup: dbRef references are not supported." And the JSON node "trace" should exist diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature deleted file mode 100644 index 980b896959a..00000000000 --- a/features/openapi/docs.feature +++ /dev/null @@ -1,440 +0,0 @@ -Feature: Documentation support - In order to build an auto-discoverable API - As a client software developer - I need to know OpenAPI specifications of objects I send and receive - - @createSchema - Scenario: Retrieve the OpenAPI documentation - Given I send a "GET" request to "/docs.json" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/json; charset=utf-8" - # Context - And the JSON node "openapi" should be equal to "3.1.0" - # Root properties - And the JSON node "info.title" should be equal to "My Dummy API" - And the JSON node "info.description" should contain "This is a test API." - And the JSON node "info.description" should contain "Made with love" - # Security Schemes - And the JSON node "components.securitySchemes" should be equal to: - """ - { - "oauth": { - "type": "oauth2", - "description": "OAuth 2.0 implicit Grant", - "flows": { - "implicit": { - "authorizationUrl": "http://my-custom-server/openid-connect/auth", - "scopes": {} - } - } - }, - "Some_Authorization_Name": { - "type": "apiKey", - "description": "Value for the Authorization header parameter.", - "name": "Authorization", - "in": "header" - } - } - """ - # Supported classes - And the OpenAPI class "AbstractDummy" exists - And the OpenAPI class "CircularReference" exists - And the OpenAPI class "CircularReference-circular" exists - And the OpenAPI class "CompositeItem" exists - And the OpenAPI class "CompositeLabel" exists - And the OpenAPI class "ConcreteDummy" exists - And the OpenAPI class "CustomIdentifierDummy" exists - And the OpenAPI class "CustomNormalizedDummy-input" exists - And the OpenAPI class "CustomNormalizedDummy-output" exists - And the OpenAPI class "CustomWritableIdentifierDummy" exists - And the OpenAPI class "Dummy" exists - And the OpenAPI class "DummyBoolean" exists - And the OpenAPI class "RelatedDummy" exists - And the OpenAPI class "DummyTableInheritance" exists - And the OpenAPI class "DummyTableInheritanceChild" exists - And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_get" exists - And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_put" exists - And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_read" exists - And the OpenAPI class "OverriddenOperationDummy-overridden_operation_dummy_write" exists - And the OpenAPI class "Person" exists - And the OpenAPI class "RelatedDummy" exists - And the OpenAPI class "NoCollectionDummy" exists - And the OpenAPI class "RelatedToDummyFriend" exists - And the OpenAPI class "RelatedToDummyFriend-fakemanytomany" exists - And the OpenAPI class "DummyFriend" exists - And the OpenAPI class "RelationEmbedder-barcelona" exists - And the OpenAPI class "RelationEmbedder-chicago" exists - And the OpenAPI class "User-user_user-read" exists - And the OpenAPI class "User-user_user-write" exists - And the OpenAPI class "UuidIdentifierDummy" exists - And the OpenAPI class "ThirdLevel" exists - And the OpenAPI class "DummyCar" exists - And the OpenAPI class "DummyWebhook" exists - And the OpenAPI class "ParentDummy" doesn't exist - And the OpenAPI class "UnknownDummy" doesn't exist - And the OpenAPI path "/relation_embedders/{id}/custom" exists - And the OpenAPI path "/override/swagger" exists - And the OpenAPI path "/api/custom-call/{id}" exists - And the JSON node "paths./api/custom-call/{id}.get" should exist - And the JSON node "paths./api/custom-call/{id}.put" should exist - # Properties - And the "id" property exists for the OpenAPI class "Dummy" - And the "name" property is required for the OpenAPI class "Dummy" - And the "genderType" property exists for the OpenAPI class "Person" - And the "genderType" property for the OpenAPI class "Person" should be equal to: - """ - { - "default": "male", - "example": "male", - "type": ["string", "null"], - "enum": [ - "male", - "female", - null - ] - } - """ - And the "playMode" property exists for the OpenAPI class "VideoGame" - And the "playMode" property for the OpenAPI class "VideoGame" should be equal to: - """ - { - "type": "string", - "format": "iri-reference", - "example": "https://example.com/" - } - """ - # Enable these tests when SF 4.4 / PHP 7.1 support is dropped - #And the "isDummyBoolean" property exists for the OpenAPI class "DummyBoolean" - #And the "isDummyBoolean" property is not read only for the OpenAPI class "DummyBoolean" - # Filters - And the JSON node "paths./dummies.get.parameters[3].name" should be equal to "dummyBoolean" - And the JSON node "paths./dummies.get.parameters[3].in" should be equal to "query" - And the JSON node "paths./dummies.get.parameters[3].required" should be false - And the JSON node "paths./dummies.get.parameters[3].schema.type" should be equal to "boolean" - - And the JSON node "paths./dummy_cars.get.parameters[8].name" should be equal to "foobar[]" - And the JSON node "paths./dummy_cars.get.parameters[8].description" should be equal to "Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: foobar[]={propertyName}&foobar[]={anotherPropertyName}&foobar[{nestedPropertyParent}][]={nestedProperty}" - - # Webhook - And the JSON node "webhooks.a.get.description" should be equal to "Something else here for example" - And the JSON node "webhooks.b.post.description" should be equal to "Hi! it's me, I'm the problem, it's me" - - # Subcollection - check filter on subResource - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].name" should be equal to "id" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].in" should be equal to "path" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].required" should be true - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[0].schema.type" should be equal to "string" - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].name" should be equal to "page" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].in" should be equal to "query" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].required" should be false - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[1].schema.type" should be equal to "integer" - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].name" should be equal to "itemsPerPage" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].in" should be equal to "query" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].required" should be false - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[2].schema.type" should be equal to "integer" - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[3].name" should be equal to "pagination" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[3].in" should be equal to "query" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[3].required" should be false - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[3].schema.type" should be equal to "boolean" - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[4].name" should be equal to "name" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[4].in" should be equal to "query" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[4].required" should be false - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[4].schema.type" should be equal to "string" - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[5].name" should be equal to "description" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[5].in" should be equal to "query" - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters[5].required" should be false - - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.parameters" should have 6 elements - - # Subcollection - check schema - And the JSON node "paths./related_dummies/{id}/related_to_dummy_friends.get.responses.200.content.application/ld+json.schema.properties.hydra:member.items.$ref" should be equal to "#/components/schemas/RelatedToDummyFriend.jsonld-fakemanytomany" - - # Deprecations - And the JSON node "paths./dummies.get.deprecated" should be false - And the JSON node "paths./deprecated_resources.get.deprecated" should be true - And the JSON node "paths./deprecated_resources.post.deprecated" should be true - And the JSON node "paths./deprecated_resources/{id}.get.deprecated" should be true - And the JSON node "paths./deprecated_resources/{id}.delete.deprecated" should be true - And the JSON node "paths./deprecated_resources/{id}.put.deprecated" should be true - And the JSON node "paths./deprecated_resources/{id}.patch.deprecated" should be true - - # Formats - And the OpenAPI class "Dummy.jsonld" exists - And the "@id" property exists for the OpenAPI class "Dummy.jsonld" - And the JSON node "paths./dummies.get.responses.200.content.application/ld+json" should be equal to: - """ - { - "schema": { - "type": "object", - "properties": { - "hydra:member": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Dummy.jsonld" - } - }, - "hydra:totalItems": { - "type": "integer", - "minimum": 0 - }, - "hydra:view": { - "type": "object", - "properties": { - "@id": { - "type": "string", - "format": "iri-reference" - }, - "@type": { - "type": "string" - }, - "hydra:first": { - "type": "string", - "format": "iri-reference" - }, - "hydra:last": { - "type": "string", - "format": "iri-reference" - }, - "hydra:previous": { - "type": "string", - "format": "iri-reference" - }, - "hydra:next": { - "type": "string", - "format": "iri-reference" - } - }, - "example": { - "@id": "string", - "type": "string", - "hydra:first": "string", - "hydra:last": "string", - "hydra:previous": "string", - "hydra:next": "string" - } - }, - "hydra:search": { - "type": "object", - "properties": { - "@type": { - "type": "string" - }, - "hydra:template": { - "type": "string" - }, - "hydra:variableRepresentation": { - "type": "string" - }, - "hydra:mapping": { - "type": "array", - "items": { - "type": "object", - "properties": { - "@type": { - "type": "string" - }, - "variable": { - "type": "string" - }, - "property": { - "type": ["string", "null"] - }, - "required": { - "type": "boolean" - } - } - } - } - } - } - }, - "required": [ - "hydra:member" - ] - } - } - """ - And the JSON node "paths./dummies.get.responses.200.content.application/json" should be equal to: - """ - { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Dummy" - } - } - } - """ - And the JSON node "paths./override_open_api_responses.post.responses" should be equal to: - """ - { - "204": { - "description": "User activated" - } - } - """ - - Scenario: OpenAPI UI is enabled for docs endpoint - Given I add "Accept" header equal to "text/html" - And I send a "GET" request to "/docs" - Then the response status code should be 200 - And I should see text matching "My Dummy API" - And I should see text matching "openapi" - - Scenario: OpenAPI extension properties is enabled in JSON docs - Given I send a "GET" request to "/docs.json" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the JSON node "paths./dummy_addresses.get.x-visibility" should be equal to "hide" - - Scenario: OpenAPI UI is enabled for an arbitrary endpoint - Given I add "Accept" header equal to "text/html" - And I send a "GET" request to "/dummies" - Then the response status code should be 200 - And I should see text matching "openapi" - - @!mongodb - Scenario: Retrieve the OpenAPI documentation with API Gateway compatibility - Given I send a "GET" request to "/docs.json?api_gateway=true" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the JSON node "basePath" should be equal to "/" - And the JSON node "components.schemas.RamseyUuidDummy.properties.id.description" should be equal to "The dummy id." - And the JSON node "components.schemas.RelatedDummy-barcelona" should not exist - And the JSON node "components.schemas.RelatedDummybarcelona" should exist - - @!mongodb - Scenario: Retrieve the OpenAPI documentation to see if shortName property is used - Given I send a "GET" request to "/docs.json" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the OpenAPI class "Resource" exists - And the OpenAPI class "ResourceRelated" exists - And the "resourceRelated" property for the OpenAPI class "Resource" should be equal to: - """ - { - "readOnly": true, - "anyOf": [ - { - "$ref": "#/components/schemas/ResourceRelated" - }, - { - "type": "null" - } - ] - } - """ - - Scenario: Retrieve the JSON OpenAPI documentation - Given I add "Accept" header equal to "application/vnd.openapi+json" - And I send a "GET" request to "/docs" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/vnd.openapi+json; charset=utf-8" - # Context - And the JSON node "openapi" should be equal to "3.1.0" - # Root properties - And the JSON node "info.title" should be equal to "My Dummy API" - And the JSON node "info.description" should contain "This is a test API." - And the JSON node "info.description" should contain "Made with love" - # Security Schemes - And the JSON node "components.securitySchemes" should be equal to: - """ - { - "oauth": { - "type": "oauth2", - "description": "OAuth 2.0 implicit Grant", - "flows": { - "implicit": { - "authorizationUrl": "http://my-custom-server/openid-connect/auth", - "scopes": {} - } - } - }, - "Some_Authorization_Name": { - "type": "apiKey", - "description": "Value for the Authorization header parameter.", - "name": "Authorization", - "in": "header" - } - } - """ - - Scenario: Retrieve the YAML OpenAPI documentation - Given I add "Accept" header equal to "application/vnd.openapi+yaml" - And I send a "GET" request to "/docs" - Then the response status code should be 200 - And the header "Content-Type" should be equal to "application/vnd.openapi+yaml; charset=utf-8" - - Scenario: Retrieve the OpenAPI documentation - Given I add "Accept" header equal to "text/html" - And I send a "GET" request to "/" - Then the response status code should be 200 - And the header "Content-Type" should be equal to "text/html; charset=utf-8" - - @!mongodb - Scenario: Retrieve the OpenAPI documentation for Entity Dto Wrappers - Given I send a "GET" request to "/docs.json" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/json; charset=utf-8" - And the OpenAPI class "WrappedResponseEntity-read" exists - And the "id" property exists for the OpenAPI class "WrappedResponseEntity-read" - And the "id" property for the OpenAPI class "WrappedResponseEntity-read" should be equal to: - """ - { - "type": "string" - } - """ - And the OpenAPI class "WrappedResponseEntity.CustomOutputEntityWrapperDto-read" exists - And the "data" property exists for the OpenAPI class "WrappedResponseEntity.CustomOutputEntityWrapperDto-read" - And the "data" property for the OpenAPI class "WrappedResponseEntity.CustomOutputEntityWrapperDto-read" should be equal to: - """ - { - "$ref": "#\/components\/schemas\/WrappedResponseEntity-read" - } - """ - - Scenario: Retrieve the OpenAPI documentation with 3.0 specification - Given I send a "GET" request to "/docs.jsonopenapi?spec_version=3.0.0" - Then the response status code should be 200 - And the response should be in JSON - And the JSON node "openapi" should be equal to "3.0.0" - And the JSON node "components.schemas.DummyBoolean.properties.id.anyOf" should be equal to: - """ - [ - { - "type": "integer" - }, - { - "type": "null" - } - ] - """ - And the JSON node "components.schemas.DummyBoolean.properties.isDummyBoolean.anyOf" should be equal to: - """ - [ - { - "type": "boolean" - }, - { - "type": "null" - } - ] - """ - And the JSON node "components.schemas.DummyBoolean.properties.isDummyBoolean.owl:maxCardinality" should not exist - - Scenario: Retrieve the OpenAPI documentation in JSON - Given I add "Accept" header equal to "text/html,*/*;q=0.8" - And I send a "GET" request to "/docs.jsonopenapi" - Then the response status code should be 200 - And the response should be in JSON diff --git a/features/openapi/entrypoint.feature b/features/openapi/entrypoint.feature deleted file mode 100644 index da1e1ae46ae..00000000000 --- a/features/openapi/entrypoint.feature +++ /dev/null @@ -1,19 +0,0 @@ -Feature: Entrypoint support - In order to build an auto-discoverable API - As a client software developer - I need to access to an entrypoint listing top-level resources - - Scenario: Retrieve the Entrypoint - When I add "Accept" header equal to "application/vnd.openapi+json" - When I send a "GET" request to "/" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/vnd.openapi+json; charset=utf-8" - And the JSON should be sorted - - Scenario: Retrieve the Entrypoint with url format - When I send a "GET" request to "/index.jsonopenapi" - Then the response status code should be 200 - And the response should be in JSON - And the header "Content-Type" should be equal to "application/vnd.openapi+json; charset=utf-8" - And the JSON should be sorted diff --git a/features/security/send_security_headers.feature b/features/security/send_security_headers.feature index 7d38893bf01..bca2f6fb9c1 100644 --- a/features/security/send_security_headers.feature +++ b/features/security/send_security_headers.feature @@ -27,6 +27,6 @@ Feature: Send security header {"name": ""} """ Then the response status code should be 422 - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the header "X-Content-Type-Options" should be equal to "nosniff" And the header "X-Frame-Options" should be equal to "deny" diff --git a/features/security/strong_typing.feature b/features/security/strong_typing.feature index 3d4b7599a62..27e669816dd 100644 --- a/features/security/strong_typing.feature +++ b/features/security/strong_typing.feature @@ -52,11 +52,10 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'The type of the "name" attribute must be "string", "NULL" given.' + And the JSON node "detail" should be equal to 'The type of the "name" attribute must be "string", "NULL" given.' Scenario: Create a resource with wrong value type for relation When I add "Content-Type" header equal to "application/ld+json" @@ -69,11 +68,10 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'Invalid IRI "1".' + And the JSON node "detail" should be equal to 'Invalid IRI "1".' And the JSON node "trace" should exist Scenario: Ignore invalid dates @@ -87,7 +85,20 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" + + Scenario: Ignore date with wrong format + When I add "Content-Type" header equal to "application/ld+json" + And I send a "POST" request to "/dummies" with body: + """ + { + "name": "Invalid date format", + "dummyDateWithFormat": "2020-01-01T00:00:00+00:00" + } + """ + Then the response status code should be 400 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" Scenario: Send non-array data when an array is expected When I add "Content-Type" header equal to "application/ld+json" @@ -100,11 +111,10 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'The type of the "relatedDummies" attribute must be "array", "string" given.' + And the JSON node "detail" should be equal to 'The type of the "relatedDummies" attribute must be "array", "string" given.' And the JSON node "trace" should exist Scenario: Send an object where an array is expected @@ -118,11 +128,10 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'The type of the key "a" must be "int", "string" given.' + And the JSON node "detail" should be equal to 'The type of the key "a" must be "int", "string" given.' Scenario: Send a scalar having the bad type When I add "Content-Type" header equal to "application/ld+json" @@ -134,11 +143,10 @@ Feature: Handle properly invalid data submitted to the API """ Then the response status code should be 400 And the response should be in JSON - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" And the JSON node "@context" should be equal to "/contexts/Error" And the JSON node "@type" should be equal to "hydra:Error" - And the JSON node "hydra:title" should be equal to "An error occurred" - And the JSON node "hydra:description" should be equal to 'The type of the "name" attribute must be "string", "integer" given.' + And the JSON node "detail" should be equal to 'The type of the "name" attribute must be "string", "integer" given.' Scenario: According to the JSON spec, allow numbers without explicit floating point for JSON formats When I add "Content-Type" header equal to "application/ld+json" diff --git a/features/security/validate_incoming_content-types.feature b/features/security/validate_incoming_content-types.feature index 993352428cc..80c6fd3b65a 100644 --- a/features/security/validate_incoming_content-types.feature +++ b/features/security/validate_incoming_content-types.feature @@ -13,5 +13,5 @@ Feature: Validate incoming content type something """ Then the response status code should be 415 - And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" - And the JSON node "hydra:description" should be equal to 'The content-type "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html", "application/graphql", "multipart/form-data".' + And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8" + And the JSON node "detail" should be equal to 'The content-type "text/plain" is not supported. Supported MIME types are "application/ld+json", "application/hal+json", "application/vnd.api+json", "application/xml", "text/xml", "application/json", "text/html", "application/graphql", "multipart/form-data".' diff --git a/features/serializer/vo_relations.feature b/features/serializer/vo_relations.feature index 0a3ba73c37a..08999440a50 100644 --- a/features/serializer/vo_relations.feature +++ b/features/serializer/vo_relations.feature @@ -150,18 +150,13 @@ Feature: Value object as ApiResource "type": "string", "pattern": "^hydra:Error$" }, - "hydra:title": { - "type": "string", - "pattern": "^An error occurred$" - }, - "hydra:description": { + "detail": { "pattern": "^Cannot create an instance of \"ApiPlatform\\\\Tests\\\\Fixtures\\\\TestBundle\\\\(Document|Entity)\\\\VoDummyCar\" from serialized data because its constructor requires the following parameters to be present : \"\\$drivers\".$" } }, "required": [ "@type", - "hydra:title", - "hydra:description" + "detail" ] } """ diff --git a/generate-changelog.sh b/generate-changelog.sh index f2903beb1b3..bf5cb10ec43 100755 --- a/generate-changelog.sh +++ b/generate-changelog.sh @@ -1,23 +1,26 @@ #!/bin/bash # usage: generate-changelog.sh previous_tag next_tag # example: generate-changelog.sh v2.7.2 v2.7.3 > CHANGELOG.new.md -log=$(git log "$1..HEAD" --pretty='format:* [%h](https://github.com/api-platform/core/commit/%H) %s' --no-merges) +lowerbranch=$(git branch --merged HEAD | grep '[[:digit:]]\.[[:digit:]]' | grep -v '*' | sort -rg | head -n 1) +log=$(git log "$1..HEAD" --no-merges --not $lowerbranch --pretty='format:* [%h](https://github.com/api-platform/core/commit/%H) %s') diff=$( printf "# Changelog\n\n" printf "## %s\n\n" "$2" -if [[ 0 != $(echo "$log" | grep fix | grep -v chore | wc -l) ]]; +fixes=$(echo "$log" | grep 'fix(\|fix:') +if [[ 0 != $(echo "$fixes" | wc -l) ]]; then printf "### Bug fixes\n\n" - printf "$log" | grep fix | grep -v chore | sort + printf "$fixes" | sort printf "\n\n" fi -if [[ 0 != $(echo "$log" | grep feat | grep -v chore | wc -l) ]]; +feat=$(echo "$log" | grep 'feat(\|feat:') +if [[ 0 != $(echo "$feat" | wc -l) ]]; then printf "### Features\n\n" - printf "$log" | grep feat | grep -v chore | sort + printf "$feat" | sort fi ) diff --git a/package-lock.json b/package-lock.json index cfae087122d..f2fc29c81f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,26 @@ { - "name": "core", + "name": "core-5433", "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { - "@fontsource/open-sans": "^5.0.28", + "@fontsource/open-sans": "^5.2.5", "es6-promise": "^4.2.8", "fetch": "^1.1.0", - "graphiql": "^3.2.2", - "graphql-playground-react": "^1.7.26", + "graphiql": "^4.1.1", + "graphql-playground-react": "^1.7.28", "react": "^18.3.1", "react-dom": "^18.3.1", - "redoc": "^2.1.4", - "swagger-ui": "^5.17.14" + "redoc": "^2.5.0", + "swagger-ui": "^5.24.0" } }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -33,6 +34,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/@ardatan/sync-fetch/-/sync-fetch-0.0.1.tgz", "integrity": "sha512-xhlTqH0m31mnsG0tIP4ETgfSB6gXDaYYsUWTrlUV93fFQPI9dd8hE0Ot6MHLCtqgB32hwJAC3YZMWlXZw7AleA==", + "license": "MIT", "dependencies": { "node-fetch": "^2.6.1" }, @@ -44,6 +46,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -59,62 +62,47 @@ } } }, - "node_modules/@ardatan/sync-fetch/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/@ardatan/sync-fetch/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/@ardatan/sync-fetch/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.6.tgz", - "integrity": "sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.6", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.6.tgz", - "integrity": "sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "license": "MIT", "peer": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.6.tgz", - "integrity": "sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "license": "MIT", "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.6", - "@babel/generator": "^7.24.6", - "@babel/helper-compilation-targets": "^7.24.6", - "@babel/helper-module-transforms": "^7.24.6", - "@babel/helpers": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/template": "^7.24.6", - "@babel/traverse": "^7.24.6", - "@babel/types": "^7.24.6", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -129,49 +117,44 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.6.tgz", - "integrity": "sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.6", + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz", - "integrity": "sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", "peer": true, "dependencies": { - "@babel/compat-data": "^7.24.6", - "@babel/helper-validator-option": "^7.24.6", - "browserslist": "^4.22.2", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -179,68 +162,46 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz", - "integrity": "sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz", - "integrity": "sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w==", "dependencies": { - "@babel/template": "^7.24.6", - "@babel/types": "^7.24.6" - }, - "engines": { - "node": ">=6.9.0" + "yallist": "^3.0.2" } }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz", - "integrity": "sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA==", - "dependencies": { - "@babel/types": "^7.24.6" - }, - "engines": { - "node": ">=6.9.0" - } + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC", + "peer": true }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz", - "integrity": "sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.24.6" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz", - "integrity": "sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "license": "MIT", "peer": true, "dependencies": { - "@babel/helper-environment-visitor": "^7.24.6", - "@babel/helper-module-imports": "^7.24.6", - "@babel/helper-simple-access": "^7.24.6", - "@babel/helper-split-export-declaration": "^7.24.6", - "@babel/helper-validator-identifier": "^7.24.6" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -250,156 +211,64 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz", - "integrity": "sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g==", - "peer": true, - "dependencies": { - "@babel/types": "^7.24.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz", - "integrity": "sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw==", - "dependencies": { - "@babel/types": "^7.24.6" - }, + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz", - "integrity": "sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz", - "integrity": "sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz", - "integrity": "sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", "peer": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.6.tgz", - "integrity": "sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "license": "MIT", "peer": true, "dependencies": { - "@babel/template": "^7.24.6", - "@babel/types": "^7.24.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.6.tgz", - "integrity": "sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.6", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/types": "^7.27.3" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.6.tgz", - "integrity": "sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q==", "bin": { "parser": "bin/babel-parser.js" }, @@ -408,11 +277,12 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -422,54 +292,51 @@ } }, "node_modules/@babel/runtime": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz", - "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.24.6.tgz", - "integrity": "sha512-tbC3o8uHK9xMgMsvUm9qGqxVpbv6yborMBLbDteHIc7JDNHsTV0vDMQ5j1O1NXvO+BDELtL9KgoWYaUVIVGt8w==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.27.6.tgz", + "integrity": "sha512-vDVrlmRAY8z9Ul/HxT+8ceAru95LQgkSKiXkSYZvqtbkPSfhZJgpRp45Cldbh1GJ1kxzQkI70AqyrTI58KpaWQ==", + "license": "MIT", "dependencies": { - "core-js-pure": "^3.30.2", - "regenerator-runtime": "^0.14.0" + "core-js-pure": "^3.30.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", - "integrity": "sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.6.tgz", - "integrity": "sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw==", - "dependencies": { - "@babel/code-frame": "^7.24.6", - "@babel/generator": "^7.24.6", - "@babel/helper-environment-visitor": "^7.24.6", - "@babel/helper-function-name": "^7.24.6", - "@babel/helper-hoist-variables": "^7.24.6", - "@babel/helper-split-export-declaration": "^7.24.6", - "@babel/parser": "^7.24.6", - "@babel/types": "^7.24.6", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -478,27 +345,23 @@ } }, "node_modules/@babel/types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.6.tgz", - "integrity": "sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.6", - "@babel/helper-validator-identifier": "^7.24.6", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@braintree/sanitize-url": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.0.2.tgz", - "integrity": "sha512-NVf/1YycDMs6+FxS0Tb/W8MjJRDQdXF+tBfDtZ5UZeiRUkTmwKc4vmYCKZTyymfJk1gnMsauvZSX/HiV9jOABw==" - }, "node_modules/@codemirror/language": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.0.0.tgz", "integrity": "sha512-rtjk5ifyMzOna1c7PBu7J1VCt0PvA5wy3o8eMVnxMKb7z8KA7JFecvD04dSn14vj/bBaAbqRsGed5OjtofEnLA==", + "license": "MIT", "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", @@ -510,69 +373,99 @@ } }, "node_modules/@codemirror/state": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", - "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==", - "peer": true + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } }, "node_modules/@codemirror/view": { - "version": "6.26.3", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.26.3.tgz", - "integrity": "sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==", + "version": "6.37.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.37.1.tgz", + "integrity": "sha512-Qy4CAUwngy/VQkEz0XzMKVRcckQuqLYWKqVpDDDghBe5FSXSqfVrJn49nw3ePZHxRUz4nRmb05Lgi+9csWo4eg==", + "license": "MIT", "peer": true, "dependencies": { - "@codemirror/state": "^6.4.0", + "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", + "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", + "license": "MIT", + "peer": true, "dependencies": { - "@emotion/memoize": "0.7.4" + "@emotion/memoize": "^0.8.1" } }, "node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "license": "MIT", + "peer": true }, "node_modules/@emotion/unitless": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==", + "license": "MIT", "peer": true }, "node_modules/@exodus/schemasafe": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@exodus/schemasafe/-/schemasafe-1.3.0.tgz", - "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==" + "integrity": "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw==", + "license": "MIT" }, "node_modules/@floating-ui/core": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.4.1.tgz", - "integrity": "sha512-jk3WqquEJRlcyu7997NtR5PibI+y5bi+LS3hPmguVClypenMsCY3CBa3LAQnozRCtCrYWSEtAdiskpamuJRFOQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz", + "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==", + "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.1.1" + "@floating-ui/utils": "^0.2.9" } }, "node_modules/@floating-ui/dom": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.2.tgz", - "integrity": "sha512-6ArmenS6qJEWmwzczWyhvrXRdI/rI78poBcW0h/456+onlabit+2G+QxHx5xTOX60NBJQXjsCLFbW2CmsXpUog==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz", + "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.4.1", - "@floating-ui/utils": "^0.1.1" + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz", - "integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.3.tgz", + "integrity": "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==", + "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.5.1" + "@floating-ui/dom": "^1.0.0" }, "peerDependencies": { "react": ">=16.8.0", @@ -580,52 +473,94 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.2.tgz", - "integrity": "sha512-ou3elfqG/hZsbmF4bxeJhPHIf3G2pm0ujc39hYEZrfVqt7Vk/Zji6CXc3W0pmYM8BW1g40U+akTl9DKZhFhInQ==" + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" }, "node_modules/@fontsource/open-sans": { - "version": "5.0.28", - "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.0.28.tgz", - "integrity": "sha512-hBvJHY76pJT/JynGUB5EXWhnzjYfLdcMn655J5p1v9lTT9HdQSy+keq2KPVXO2Htlg998BBa3p6u/jlrZ6w0kg==" + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@fontsource/open-sans/-/open-sans-5.2.5.tgz", + "integrity": "sha512-f0Ww6H+LB6GXA8UCgqs90h4djVttu3quH/1+wkfUY8b09mG1ESn4ACRBHYY78bsoeDXpaCyZh7eoGROBWplvAQ==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@graphiql/plugin-doc-explorer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@graphiql/plugin-doc-explorer/-/plugin-doc-explorer-0.2.2.tgz", + "integrity": "sha512-0Pj0vsNFfZJZJ3moC7O9xF1Dt2KWyvdxke+cFFZQLN1L2VxdTUa4cfGSg5ujv/ikDUUyyPQNTZuyXzagM4NG5g==", + "license": "MIT", + "dependencies": { + "@graphiql/react": "^0.34.1", + "@headlessui/react": "^2.2", + "react-compiler-runtime": "19.1.0-rc.1", + "zustand": "^5" + }, + "peerDependencies": { + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } + }, + "node_modules/@graphiql/plugin-history": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@graphiql/plugin-history/-/plugin-history-0.2.2.tgz", + "integrity": "sha512-ta1k8ichGVfMg6eDwRYa/2e92PfLPh+wgtJTuTQagmXBLXhM7g8Hbk+CaE2clUahQ8/4CZOIRXJHbQ7B5tI9dg==", + "license": "MIT", + "dependencies": { + "@graphiql/react": "^0.34.1", + "@graphiql/toolkit": "^0.11.3", + "react-compiler-runtime": "19.1.0-rc.1", + "zustand": "^5" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + } }, "node_modules/@graphiql/react": { - "version": "0.22.1", - "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.22.1.tgz", - "integrity": "sha512-PBClhO2juCvVvmE5qD4PHivJLkhp0dqIX1zgId8Z83UCKpxO2M+bEspRL9aOQQaE4F4xqExCUk5B2AL+wc+agg==", - "dependencies": { - "@graphiql/toolkit": "^0.9.1", - "@headlessui/react": "^1.7.15", - "@radix-ui/react-dialog": "^1.0.4", - "@radix-ui/react-dropdown-menu": "^2.0.5", - "@radix-ui/react-tooltip": "^1.0.6", - "@radix-ui/react-visually-hidden": "^1.0.3", + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@graphiql/react/-/react-0.34.1.tgz", + "integrity": "sha512-Ykqt5uzIRKcbriyVisr/y6BwxK9ZIsilaJFPbajDAcoDiXlGEx/Pl86OcJNjslsqn79M9BkrphWvWa7cBGz3Sw==", + "license": "MIT", + "dependencies": { + "@graphiql/toolkit": "^0.11.3", + "@radix-ui/react-dialog": "^1.1", + "@radix-ui/react-dropdown-menu": "^2.1", + "@radix-ui/react-tooltip": "^1.2", + "@radix-ui/react-visually-hidden": "^1.2", "@types/codemirror": "^5.60.8", "clsx": "^1.2.1", "codemirror": "^5.65.3", - "codemirror-graphql": "^2.0.11", + "codemirror-graphql": "^2.2.1", "copy-to-clipboard": "^3.2.0", - "framer-motion": "^6.5.1", - "graphql-language-service": "^5.2.0", + "framer-motion": "^12", + "get-value": "^3.0.1", + "graphql-language-service": "^5.3.1", "markdown-it": "^14.1.0", - "set-value": "^4.1.0" + "react-compiler-runtime": "19.1.0-rc.1", + "set-value": "^4.1.0", + "zustand": "^5" }, "peerDependencies": { - "graphql": "^15.5.0 || ^16.0.0", - "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" } }, "node_modules/@graphiql/toolkit": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.9.1.tgz", - "integrity": "sha512-LVt9pdk0830so50ZnU2Znb2rclcoWznG8r8asqAENzV0U1FM1kuY0sdPpc/rBc9MmmNgnB6A+WZzDhq6dbhTHA==", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.11.3.tgz", + "integrity": "sha512-Glf0fK1cdHLNq52UWPzfSrYIJuNxy8h4451Pw1ZVpJ7dtU+tm7GVVC64UjEDQ/v2j3fnG4cX8jvR75IvfL6nzQ==", + "license": "MIT", "dependencies": { "@n1ru4l/push-pull-async-iterable-iterator": "^3.1.0", "meros": "^1.1.4" }, "peerDependencies": { - "graphql": "^15.5.0 || ^16.0.0", + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", "graphql-ws": ">= 4.5.0" }, "peerDependenciesMeta": { @@ -638,6 +573,7 @@ "version": "8.5.22", "resolved": "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.5.22.tgz", "integrity": "sha512-hcV1JaY6NJQFQEwCKrYhpfLK8frSXDbtNMoTur98u10Cmecy1zrqNKSqhEyGetpgHxaJRqszGzKeI3RuroDN6A==", + "license": "MIT", "dependencies": { "@graphql-tools/utils": "^9.2.1", "dataloader": "^2.2.2", @@ -652,6 +588,7 @@ "version": "9.0.35", "resolved": "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-9.0.35.tgz", "integrity": "sha512-jwPu8NJbzRRMqi4Vp/5QX1vIUeUPpWmlQpOkXQD2r1X45YsVceyUUBnktCrlJlDB4jPRVy7JQGwmYo3KFiOBMA==", + "license": "MIT", "dependencies": { "@graphql-tools/batch-execute": "^8.5.22", "@graphql-tools/executor": "^0.0.20", @@ -669,6 +606,7 @@ "version": "0.0.20", "resolved": "https://registry.npmjs.org/@graphql-tools/executor/-/executor-0.0.20.tgz", "integrity": "sha512-GdvNc4vszmfeGvUqlcaH1FjBoguvMYzxAfT6tDd4/LgwymepHhinqLNA5otqwVLW+JETcDaK7xGENzFomuE6TA==", + "license": "MIT", "dependencies": { "@graphql-tools/utils": "^9.2.1", "@graphql-typed-document-node/core": "3.2.0", @@ -684,6 +622,7 @@ "version": "0.0.14", "resolved": "https://registry.npmjs.org/@graphql-tools/executor-graphql-ws/-/executor-graphql-ws-0.0.14.tgz", "integrity": "sha512-P2nlkAsPZKLIXImFhj0YTtny5NQVGSsKnhi7PzXiaHSXc6KkzqbWZHKvikD4PObanqg+7IO58rKFpGXP7eeO+w==", + "license": "MIT", "dependencies": { "@graphql-tools/utils": "^9.2.1", "@repeaterjs/repeater": "3.0.4", @@ -697,10 +636,17 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@graphql-tools/executor-graphql-ws/node_modules/@repeaterjs/repeater": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz", + "integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==", + "license": "MIT" + }, "node_modules/@graphql-tools/executor-graphql-ws/node_modules/ws": { "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -721,6 +667,7 @@ "version": "0.1.10", "resolved": "https://registry.npmjs.org/@graphql-tools/executor-http/-/executor-http-0.1.10.tgz", "integrity": "sha512-hnAfbKv0/lb9s31LhWzawQ5hghBfHS+gYWtqxME6Rl0Aufq9GltiiLBcl7OVVOnkLF0KhwgbYP1mB5VKmgTGpg==", + "license": "MIT", "dependencies": { "@graphql-tools/utils": "^9.2.1", "@repeaterjs/repeater": "^3.0.4", @@ -739,6 +686,7 @@ "version": "0.0.11", "resolved": "https://registry.npmjs.org/@graphql-tools/executor-legacy-ws/-/executor-legacy-ws-0.0.11.tgz", "integrity": "sha512-4ai+NnxlNfvIQ4c70hWFvOZlSUN8lt7yc+ZsrwtNFbFPH/EroIzFMapAxM9zwyv9bH38AdO3TQxZ5zNxgBdvUw==", + "license": "MIT", "dependencies": { "@graphql-tools/utils": "^9.2.1", "@types/ws": "^8.0.0", @@ -754,6 +702,7 @@ "version": "8.13.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -774,6 +723,7 @@ "version": "7.5.17", "resolved": "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.5.17.tgz", "integrity": "sha512-hVwwxPf41zOYgm4gdaZILCYnKB9Zap7Ys9OhY1hbwuAuC4MMNY9GpUjoTU3CQc3zUiPoYStyRtUGkHSJZ3HxBw==", + "license": "MIT", "dependencies": { "@graphql-tools/import": "6.7.18", "@graphql-tools/utils": "^9.2.1", @@ -789,6 +739,7 @@ "version": "6.7.18", "resolved": "https://registry.npmjs.org/@graphql-tools/import/-/import-6.7.18.tgz", "integrity": "sha512-XQDdyZTp+FYmT7as3xRWH/x8dx0QZA2WZqfMF5EWb36a0PiH7WwlRQYIdyYXj8YCLpiWkeBXgBRHmMnwEYR8iQ==", + "license": "MIT", "dependencies": { "@graphql-tools/utils": "^9.2.1", "resolve-from": "5.0.0", @@ -802,6 +753,7 @@ "version": "7.4.18", "resolved": "https://registry.npmjs.org/@graphql-tools/json-file-loader/-/json-file-loader-7.4.18.tgz", "integrity": "sha512-AJ1b6Y1wiVgkwsxT5dELXhIVUPs/u3VZ8/0/oOtpcoyO/vAeM5rOvvWegzicOOnQw8G45fgBRMkkRfeuwVt6+w==", + "license": "MIT", "dependencies": { "@graphql-tools/utils": "^9.2.1", "globby": "^11.0.3", @@ -816,6 +768,7 @@ "version": "7.8.14", "resolved": "https://registry.npmjs.org/@graphql-tools/load/-/load-7.8.14.tgz", "integrity": "sha512-ASQvP+snHMYm+FhIaLxxFgVdRaM0vrN9wW2BKInQpktwWTXVyk+yP5nQUCEGmn0RTdlPKrffBaigxepkEAJPrg==", + "license": "MIT", "dependencies": { "@graphql-tools/schema": "^9.0.18", "@graphql-tools/utils": "^9.2.1", @@ -830,6 +783,7 @@ "version": "8.4.2", "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.4.2.tgz", "integrity": "sha512-XbrHAaj8yDuINph+sAfuq3QCZ/tKblrTLOpirK0+CAgNlZUCHs0Fa+xtMUURgwCVThLle1AF7svJCxFizygLsw==", + "license": "MIT", "dependencies": { "@graphql-tools/utils": "^9.2.1", "tslib": "^2.4.0" @@ -842,6 +796,7 @@ "version": "9.0.19", "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.19.tgz", "integrity": "sha512-oBRPoNBtCkk0zbUsyP4GaIzCt8C0aCI4ycIRUL67KK5pOHljKLBBtGT+Jr6hkzA74C8Gco8bpZPe7aWFjiaK2w==", + "license": "MIT", "dependencies": { "@graphql-tools/merge": "^8.4.1", "@graphql-tools/utils": "^9.2.1", @@ -856,6 +811,7 @@ "version": "7.17.18", "resolved": "https://registry.npmjs.org/@graphql-tools/url-loader/-/url-loader-7.17.18.tgz", "integrity": "sha512-ear0CiyTj04jCVAxi7TvgbnGDIN2HgqzXzwsfcqiVg9cvjT40NcMlZ2P1lZDgqMkZ9oyLTV8Bw6j+SyG6A+xPw==", + "license": "MIT", "dependencies": { "@ardatan/sync-fetch": "^0.0.1", "@graphql-tools/delegate": "^9.0.31", @@ -875,10 +831,32 @@ "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@graphql-tools/url-loader/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@graphql-tools/utils": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz", "integrity": "sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==", + "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "tslib": "^2.4.0" @@ -891,6 +869,7 @@ "version": "9.4.2", "resolved": "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-9.4.2.tgz", "integrity": "sha512-DFcd9r51lmcEKn0JW43CWkkI2D6T9XI1juW/Yo86i04v43O9w2/k4/nx2XTJv4Yv+iXwUw7Ok81PGltwGJSDSA==", + "license": "MIT", "dependencies": { "@graphql-tools/delegate": "^9.0.31", "@graphql-tools/schema": "^9.0.18", @@ -906,86 +885,36 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "node_modules/@headlessui/react": { - "version": "1.7.17", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.7.17.tgz", - "integrity": "sha512-4am+tzvkqDSSgiwrsEpGWqgGo9dz8qU5M3znCkC4PgkpY4HcCZzEDEvozltGGGHIKl9jbXbZPSH5TWn4sWJdow==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.4.tgz", + "integrity": "sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==", + "license": "MIT", "dependencies": { - "client-only": "^0.0.1" + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "react": "^16 || ^17 || ^18", - "react-dom": "^16 || ^17 || ^18" - } - }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -996,9 +925,10 @@ } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", "engines": { "node": ">=6.0.0" } @@ -1007,110 +937,66 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@lezer/common": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz", - "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "license": "MIT", "peer": true }, "node_modules/@lezer/highlight": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz", - "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "license": "MIT", "peer": true, "dependencies": { "@lezer/common": "^1.0.0" } }, "node_modules/@lezer/lr": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.1.tgz", - "integrity": "sha512-CHsKq8DMKBf9b3yXPDIU4DbH+ZJd/sJdYOW2llbW/HudP5u0VS6Bfq1hLYfgU7uAYGFIyGGQIsSOXGPEErZiJw==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "license": "MIT", "peer": true, "dependencies": { "@lezer/common": "^1.0.0" } }, - "node_modules/@motionone/animation": { - "version": "10.15.1", - "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.15.1.tgz", - "integrity": "sha512-mZcJxLjHor+bhcPuIFErMDNyrdb2vJur8lSfMCsuCB4UyV8ILZLvK+t+pg56erv8ud9xQGK/1OGPt10agPrCyQ==", - "dependencies": { - "@motionone/easing": "^10.15.1", - "@motionone/types": "^10.15.1", - "@motionone/utils": "^10.15.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/dom": { - "version": "10.12.0", - "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.12.0.tgz", - "integrity": "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw==", - "dependencies": { - "@motionone/animation": "^10.12.0", - "@motionone/generators": "^10.12.0", - "@motionone/types": "^10.12.0", - "@motionone/utils": "^10.12.0", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/easing": { - "version": "10.15.1", - "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.15.1.tgz", - "integrity": "sha512-6hIHBSV+ZVehf9dcKZLT7p5PEKHGhDwky2k8RKkmOvUoYP3S+dXsKupyZpqx5apjd9f+php4vXk4LuS+ADsrWw==", - "dependencies": { - "@motionone/utils": "^10.15.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/generators": { - "version": "10.15.1", - "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.15.1.tgz", - "integrity": "sha512-67HLsvHJbw6cIbLA/o+gsm7h+6D4Sn7AUrB/GPxvujse1cGZ38F5H7DzoH7PhX+sjvtDnt2IhFYF2Zp1QTMKWQ==", - "dependencies": { - "@motionone/types": "^10.15.1", - "@motionone/utils": "^10.15.1", - "tslib": "^2.3.1" - } - }, - "node_modules/@motionone/types": { - "version": "10.15.1", - "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.15.1.tgz", - "integrity": "sha512-iIUd/EgUsRZGrvW0jqdst8st7zKTzS9EsKkP+6c6n4MPZoQHwiHuVtTQLD6Kp0bsBLhNzKIBlHXponn/SDT4hA==" - }, - "node_modules/@motionone/utils": { - "version": "10.15.1", - "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.15.1.tgz", - "integrity": "sha512-p0YncgU+iklvYr/Dq4NobTRdAPv9PveRDUXabPEeOjBLSO/1FNB2phNTZxOxpi1/GZwYpAoECEa0Wam+nsmhSw==", - "dependencies": { - "@motionone/types": "^10.15.1", - "hey-listen": "^1.0.8", - "tslib": "^2.3.1" - } + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT", + "peer": true }, "node_modules/@n1ru4l/push-pull-async-iterable-iterator": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz", "integrity": "sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q==", + "license": "MIT", "engines": { "node": ">=12" } @@ -1119,6 +1005,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1131,6 +1018,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", "engines": { "node": ">= 8" } @@ -1139,6 +1027,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1148,19 +1037,21 @@ } }, "node_modules/@peculiar/asn1-schema": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.6.tgz", - "integrity": "sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==", + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz", + "integrity": "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==", + "license": "MIT", "dependencies": { "asn1js": "^3.0.5", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.0" + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" } }, "node_modules/@peculiar/json-schema": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -1169,41 +1060,40 @@ } }, "node_modules/@peculiar/webcrypto": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.4.3.tgz", - "integrity": "sha512-VtaY4spKTdN5LjJ04im/d/joXuvLbQdgy5Z4DXF4MFZhQ+MTrejbNMkfZBp1Bs3O5+bFqnJgyGdPuZQflvIa5A==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz", + "integrity": "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==", + "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/asn1-schema": "^2.3.8", "@peculiar/json-schema": "^1.1.12", - "pvtsutils": "^1.3.2", - "tslib": "^2.5.0", - "webcrypto-core": "^1.7.7" + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2", + "webcrypto-core": "^1.8.0" }, "engines": { "node": ">=10.12.0" } }, "node_modules/@radix-ui/primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", - "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - } + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" }, "node_modules/@radix-ui/react-arrow": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", - "integrity": "sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1215,21 +1105,21 @@ } }, "node_modules/@radix-ui/react-collection": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz", - "integrity": "sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1241,15 +1131,13 @@ } }, "node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1258,15 +1146,13 @@ } }, "node_modules/@radix-ui/react-context": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", - "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1275,31 +1161,31 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.4.tgz", - "integrity": "sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.3", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-portal": "1.0.3", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1311,15 +1197,13 @@ } }, "node_modules/@radix-ui/react-direction": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", - "integrity": "sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1328,22 +1212,22 @@ } }, "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.4.tgz", - "integrity": "sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-escape-keydown": "1.0.3" + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1355,24 +1239,24 @@ } }, "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.5.tgz", - "integrity": "sha512-xdOrZzOTocqqkCkYo8yRPCib5OkTkqN7lqNCdxwPOdE466DOaNl4N8PkUIlsXthQvW5Wwkd+aEmWpfWlBoDPEw==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-menu": "2.0.5", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-controllable-state": "1.0.1" + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", + "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1384,15 +1268,13 @@ } }, "node_modules/@radix-ui/react-focus-guards": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz", - "integrity": "sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1401,20 +1283,20 @@ } }, "node_modules/@radix-ui/react-focus-scope": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.3.tgz", - "integrity": "sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1426,16 +1308,16 @@ } }, "node_modules/@radix-ui/react-id": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", - "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1444,35 +1326,35 @@ } }, "node_modules/@radix-ui/react-menu": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.5.tgz", - "integrity": "sha512-Gw4f9pwdH+w5w+49k0gLjN0PfRDHvxmAgG16AbyJZ7zhwZ6PBHKtWohvnSwfusfnK3L68dpBREHpVkj8wEM7ZA==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.3", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.2", - "@radix-ui/react-portal": "1.0.3", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-callback-ref": "1.0.1", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", + "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1484,27 +1366,27 @@ } }, "node_modules/@radix-ui/react-popper": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.2.tgz", - "integrity": "sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1", - "@radix-ui/react-use-rect": "1.0.1", - "@radix-ui/react-use-size": "1.0.1", - "@radix-ui/rect": "1.0.1" + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1516,18 +1398,19 @@ } }, "node_modules/@radix-ui/react-portal": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.3.tgz", - "integrity": "sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1539,19 +1422,19 @@ } }, "node_modules/@radix-ui/react-presence": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.0.1.tgz", - "integrity": "sha512-UXLW4UAbIY5ZjcvzjfRFo5gxva8QirC9hF7wRE4U5gz+TP0DbRk+//qyuAQ1McDxBt1xNMBTaciFGvEmJvAZCg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1563,18 +1446,18 @@ } }, "node_modules/@radix-ui/react-primitive": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", - "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1586,26 +1469,26 @@ } }, "node_modules/@radix-ui/react-roving-focus": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz", - "integrity": "sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-collection": "1.0.3", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-use-callback-ref": "1.0.1", - "@radix-ui/react-use-controllable-state": "1.0.1" + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1617,16 +1500,16 @@ } }, "node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" + "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1635,29 +1518,29 @@ } }, "node_modules/@radix-ui/react-tooltip": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.6.tgz", - "integrity": "sha512-DmNFOiwEc2UDigsYj6clJENma58OelxD24O4IODoZ+3sQc3Zb+L8w1EP+y9laTuKCLAysPw4fD6/v0j4KNV8rg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-popper": "1.1.2", - "@radix-ui/react-portal": "1.0.3", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", - "@radix-ui/react-visually-hidden": "1.0.3" + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1669,15 +1552,32 @@ } }, "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", - "integrity": "sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10" + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1685,17 +1585,17 @@ } } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz", - "integrity": "sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==", + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1704,16 +1604,16 @@ } }, "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz", - "integrity": "sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-callback-ref": "1.0.1" + "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1722,15 +1622,13 @@ } }, "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", - "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1739,16 +1637,16 @@ } }, "node_modules/@radix-ui/react-use-rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz", - "integrity": "sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/rect": "1.0.1" + "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1757,16 +1655,16 @@ } }, "node_modules/@radix-ui/react-use-size": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz", - "integrity": "sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-use-layout-effect": "1.0.1" + "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1775,18 +1673,18 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", - "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.3" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -1798,22 +1696,136 @@ } }, "node_modules/@radix-ui/rect": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", - "integrity": "sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@react-aria/focus": { + "version": "3.20.4", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.4.tgz", + "integrity": "sha512-E9M/kPYvF1fBZpkRXsKqMhvBVEyTY7vmkHeXLJo6tInKQOjYyYs0VeWlnGnxBjQIAH7J7ZKAORfTFQQHyhoueQ==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.25.2", + "@react-aria/utils": "^3.29.1", + "@react-types/shared": "^3.30.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/focus/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.2.tgz", + "integrity": "sha512-BWyZXBT4P17b9C9HfOIT2glDFMH9nUCfQF7vZ5FEeXNBudH/8OcSbzyBUG4Dg3XPtkOem5LP59ocaizkl32Tvg==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.9", + "@react-aria/utils": "^3.29.1", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.30.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz", + "integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.1.tgz", + "integrity": "sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.9", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.7", + "@react-types/shared": "^3.30.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.7.tgz", + "integrity": "sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==", + "license": "Apache-2.0", "dependencies": { - "@babel/runtime": "^7.13.10" + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.30.0.tgz", + "integrity": "sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/@redocly/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-9GWx27t7xWhDIR02PA18nzBdLcKQRgc46xNQvjFkrYk4UOmvKhJ/dawwiX0cCOeetN5LcaaiqQbVOWYK62SGHw==", + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js-replace": "^1.0.1" }, "funding": { "type": "github", @@ -1821,40 +1833,43 @@ } }, "node_modules/@redocly/config": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.5.0.tgz", - "integrity": "sha512-oA1ezWPT2tSV9CLk0FtZlViaFKtp+id3iAVeKBme1DdP4xUCdxEdP8umB21iLKdc6leRd5uGa+T5Ox4nHBAXWg==" + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz", + "integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==", + "license": "MIT" }, "node_modules/@redocly/openapi-core": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.14.0.tgz", - "integrity": "sha512-sraF4PGVcc6t6CaYw5raO/GWeOaa6UjcEvH/+Qm7zp+q/fbWAMwbj+1QzaNvpMspCwF+xW6TddDcnXrCDmqYVA==", + "version": "1.34.3", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.3.tgz", + "integrity": "sha512-3arRdUp1fNx55itnjKiUhO6t4Mf91TsrTIYINDNLAZPS0TPd5YpiXRctwjel0qqWoOOhjA34cZ3m4dksLDFUYg==", + "license": "MIT", "dependencies": { - "@redocly/ajv": "^8.11.0", - "@redocly/config": "^0.5.0", + "@redocly/ajv": "^8.11.2", + "@redocly/config": "^0.22.0", "colorette": "^1.2.0", + "https-proxy-agent": "^7.0.5", "js-levenshtein": "^1.1.6", "js-yaml": "^4.1.0", - "lodash.isequal": "^4.5.0", "minimatch": "^5.0.1", - "node-fetch": "^2.6.1", "pluralize": "^8.0.0", "yaml-ast-parser": "0.0.43" }, "engines": { - "node": ">=14.19.0", - "npm": ">=7.0.0" + "node": ">=18.17.0", + "npm": ">=9.5.0" } }, "node_modules/@redocly/openapi-core/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/@redocly/openapi-core/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -1862,48 +1877,11 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/@redocly/openapi-core/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/@redocly/openapi-core/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/@redocly/openapi-core/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/@redocly/openapi-core/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/@redux-saga/core": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.3.tgz", - "integrity": "sha512-U1JO6ncFBAklFTwoQ3mjAeQZ6QGutsJzwNBjgVLSWDpZTRhobUzuVDS1qH3SKGJD8fvqoaYOjp6XJ3gCmeZWgA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz", + "integrity": "sha512-L+i+qIGuyWn7CIg7k1MteHGfttKPmxwZR5E7OsGikCL2LzYA0RERlaUY00Y3P3ZV2EYgrsYlBrGs6cJP5OKKqA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.6.3", "@redux-saga/deferred": "^1.2.1", @@ -1911,7 +1889,6 @@ "@redux-saga/is": "^1.1.3", "@redux-saga/symbols": "^1.1.3", "@redux-saga/types": "^1.2.1", - "redux": "^4.0.4", "typescript-tuple": "^2.2.1" }, "funding": { @@ -1922,12 +1899,14 @@ "node_modules/@redux-saga/deferred": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz", - "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==" + "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==", + "license": "MIT" }, "node_modules/@redux-saga/delay-p": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz", "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==", + "license": "MIT", "dependencies": { "@redux-saga/symbols": "^1.1.3" } @@ -1936,6 +1915,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", + "license": "MIT", "dependencies": { "@redux-saga/symbols": "^1.1.3", "@redux-saga/types": "^1.2.1" @@ -1944,46 +1924,36 @@ "node_modules/@redux-saga/symbols": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", - "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" + "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==", + "license": "MIT" }, "node_modules/@redux-saga/types": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", - "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" + "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==", + "license": "MIT" }, "node_modules/@repeaterjs/repeater": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.4.tgz", - "integrity": "sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dependencies": { - "type-detect": "4.0.8" - } + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.0.6.tgz", + "integrity": "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==", + "license": "MIT" }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" }, "node_modules/@swagger-api/apidom-ast": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-1.0.0-alpha.5.tgz", - "integrity": "sha512-ZH3xryzmwd8OvUdOJH4ujNAyQMXN6NCrRT0HGR8z9TnA0nFPFoOAswq7317mCn77VJmViu/tpCuvmRS0a9BROg==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ast/-/apidom-ast-1.0.0-beta.42.tgz", + "integrity": "sha512-Uu+qqX6tSIkwlX3u1YLn47whFkhTxuEfAUJLMCdC3c8P2usRMW5iji8eSy7UMU5I0Ou+twYtZgM55/51ohC6Gg==", + "license": "Apache-2.0", "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-error": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-error": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -1991,52 +1961,70 @@ } }, "node_modules/@swagger-api/apidom-core": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-1.0.0-alpha.5.tgz", - "integrity": "sha512-iArtPxwcQ/EpQU/VqwBDrD+F0lngyUyLVCa8zR4gT+7mP6fpiU7jcerizw0hDpFmvieXddx5UdfO28Pxuq204g==", - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^1.0.0-alpha.5", - "@swagger-api/apidom-error": "^1.0.0-alpha.5", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-core/-/apidom-core-1.0.0-beta.42.tgz", + "integrity": "sha512-X0IELPVS2Wo+RcZ8pVoOfLF8o/52CGrve+0QogFADZkN2ai3J7Gfr0ni6YWJdHMBy6T8X72yNmVaH/gwgcpTYg==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "minim": "~0.23.8", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", - "short-unique-id": "^5.0.2", + "short-unique-id": "^5.3.2", "ts-mixer": "^6.0.3" } }, "node_modules/@swagger-api/apidom-error": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-error/-/apidom-error-1.0.0-alpha.5.tgz", - "integrity": "sha512-5UEgSZuQPdkqKSKDtRXQ0cm7x1o4EPyusLBVsCG4l8QtJvAhG1OOpEzJbTZ48/nRt7VkbK7MTj/up+oEILzVvw==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-error/-/apidom-error-1.0.0-beta.42.tgz", + "integrity": "sha512-rb8BwdnMgpF4MGfiNwefg3RM5dUmvRfLSYabezH0PACtYu6FuKBmQvY03KCgU4yfZAh99CffLg4i350tusOf1w==", + "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.20.7" } }, "node_modules/@swagger-api/apidom-json-pointer": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-1.0.0-alpha.5.tgz", - "integrity": "sha512-eDAz7/UaGpGCvB0y1GoRjFwxFWseCsF/0ZYIQvvq9PS025inc/I6M+XX8dWMmkpNpbbf+KfD7WlwfqnUZLv/MQ==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-json-pointer/-/apidom-json-pointer-1.0.0-beta.42.tgz", + "integrity": "sha512-rh1YisOtAnJXCLjdFY0EcBKxsQoM7rd2M6AFDRMIic9RuOsTA7aAaMnNT//tnKp3Q+fTCkCVFL082Z3RTwCACQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", + "@swaggerexpert/json-pointer": "^2.10.1" + } + }, + "node_modules/@swagger-api/apidom-ns-api-design-systems": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-1.0.0-beta.42.tgz", + "integrity": "sha512-CjsIYwYbjSJ4EoIH3BY4J5rboQEjXB2BTvIb50FahadzKc70RAWpanrpgBApKPdyyGL1Hd9Ef33e8tJ9pY1jog==", + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-error": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", - "ramda-adjunct": "^5.0.0" + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.3" } }, - "node_modules/@swagger-api/apidom-ns-api-design-systems": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-api-design-systems/-/apidom-ns-api-design-systems-1.0.0-alpha.5.tgz", - "integrity": "sha512-aq9Ix2Wo2TMfYW3HmheTO3qVd2MYrdinjLFHn9uozzC2x+CSzALhvKkwOc29HiGOn4QQ6QHHPRojNgD86WkwUg==", + "node_modules/@swagger-api/apidom-ns-arazzo-1": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-arazzo-1/-/apidom-ns-arazzo-1-1.0.0-beta.42.tgz", + "integrity": "sha512-4WAKYPxu6ufpyTKWjiEHF8LV5KU+cY0Wj1KgHpLnIwvJjxWuCoAsGdtjaHYtRJearSTIvxgqGYRy7YJDRIWxVg==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-error": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-json-schema-2020-12": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -2044,44 +2032,78 @@ } }, "node_modules/@swagger-api/apidom-ns-asyncapi-2": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-1.0.0-alpha.5.tgz", - "integrity": "sha512-JFtQBhCOkYuyNVcYGMFd9+U0UO6lEj9kO5qCgUjPOTgkOpZOZQslVEtg3TDmRlBATwVdmRv39xy3ZLK8O/JdmQ==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-asyncapi-2/-/apidom-ns-asyncapi-2-1.0.0-beta.42.tgz", + "integrity": "sha512-aqIoVAFoj5h7rBFhTIR5KEfuD7Xl4T2byCbJPhXaEuA8y/VxLlFBXMjLQBXfq3x/VjRUMNtsk6SONlxCoM15iw==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-json-schema-draft-7": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-json-schema-draft-7": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", "ts-mixer": "^6.0.3" } }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-4": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-1.0.0-alpha.5.tgz", - "integrity": "sha512-aDmcpGikL5JZmDTg7J6EJfLFjtUmX/MfduS4hQeopFCkw91dZsqxO10j7KEiRVVuJBuGStbYoHI5aIsQTlebzA==", + "node_modules/@swagger-api/apidom-ns-json-schema-2019-09": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-2019-09/-/apidom-ns-json-schema-2019-09-1.0.0-beta.42.tgz", + "integrity": "sha512-FlyXopkBvUGDxuLD9yMoRkDR4wi+tYdnlg0ZTxCYHeQS1M9hrujGfuZGYDFaF6YeFwIDoOP3kr69uhho3dh4FQ==", + "license": "Apache-2.0", "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^1.0.0-alpha.5", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-json-schema-draft-7": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", "ts-mixer": "^6.0.4" } }, - "node_modules/@swagger-api/apidom-ns-json-schema-draft-6": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-1.0.0-alpha.5.tgz", - "integrity": "sha512-ylh96E59aaV1VDv9sDrNwpTmjVT6vmOSncpmytlc0ynb374dwZkLZ63Hd30rcMFAhKmg5aYOG+i5O1QXKFYz8A==", - "optional": true, + "node_modules/@swagger-api/apidom-ns-json-schema-2020-12": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-2020-12/-/apidom-ns-json-schema-2020-12-1.0.0-beta.42.tgz", + "integrity": "sha512-t64FV0SpIOFwVfl1u+Y0438oq8kz0GerL8bfaw23OzAwvVN4iebSgXIOZ606agGJbAUrEkseKbVockZ68mdwvw==", + "license": "Apache-2.0", "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-error": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-json-schema-2019-09": "^1.0.0-beta.42", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-draft-4": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-4/-/apidom-ns-json-schema-draft-4-1.0.0-beta.42.tgz", + "integrity": "sha512-t4Y6mOXabUV8qYlWNa32NqdKTYO800SIkbfEGg42Wv3lRXO1tPYeRsyujp/h8xtKGwcIuBc0/Sr6nx8wMMAmVA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.0.0-beta.42", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0", + "ts-mixer": "^6.0.4" + } + }, + "node_modules/@swagger-api/apidom-ns-json-schema-draft-6": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-6/-/apidom-ns-json-schema-draft-6-1.0.0-beta.42.tgz", + "integrity": "sha512-OA4Rj+Aq7W+lQXrDrnChHHXKWUltf75knFx5mx94E7mKFGWKn5GbpnDAtMCyyEhWC4aq8e3oF2MuJOM52GWKrQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -2089,15 +2111,15 @@ } }, "node_modules/@swagger-api/apidom-ns-json-schema-draft-7": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-1.0.0-alpha.5.tgz", - "integrity": "sha512-Mks9gabJvz4atkjzLDwjWbo12xirul7a9ifHYZQJc/jfVKfVNy1e3QgFG1+EbSWWG5Yfbr3WKyxUDJLgr75qKg==", - "optional": true, - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-error": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-json-schema-draft-6": "^1.0.0-alpha.5", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-json-schema-draft-7/-/apidom-ns-json-schema-draft-7-1.0.0-beta.42.tgz", + "integrity": "sha512-nGRRIQBfjbp+NHb3QwGa/VBQcR9ojJv2OyHF2cDZNLqqnfwNE8FcQElIngSHxFhqmjTNWix4zQVNEq819TDXbA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-json-schema-draft-6": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -2105,15 +2127,16 @@ } }, "node_modules/@swagger-api/apidom-ns-openapi-2": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-1.0.0-alpha.5.tgz", - "integrity": "sha512-uY+1G4oRf9UT/6sGuatvWKstmlRnEiN9XqaVvV8euXESxI4jtwcPbRwoEX31vEYXoTqq2ZScFy8UQJ2CJ2ZADw==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-2/-/apidom-ns-openapi-2-1.0.0-beta.42.tgz", + "integrity": "sha512-63+VhaB9mejbTgeXCNt4mlqlAbcSqaYrHmBJazI2E2nc6vv0UFtjDSkhV5WUp9iuWJaEo5F7og07m3af0i7DNA==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-error": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -2121,14 +2144,15 @@ } }, "node_modules/@swagger-api/apidom-ns-openapi-3-0": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-1.0.0-alpha.5.tgz", - "integrity": "sha512-UAOGZaGMDVRQ10l8OgXCAfxS9PxGoCW66o/vFmhPfrK8NwU1GEo6sYHYoo1mflNMHCN2eVYyM5LxA+qYm0SJgQ==", - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-error": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-alpha.5", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-0/-/apidom-ns-openapi-3-0-1.0.0-beta.42.tgz", + "integrity": "sha512-VOGao/S625wG0VMFDB9FnDAjvK+xJSpeyBhAnef/jUFFO3hn7Kpiil8gJ91m+/2cFkTJietRcwXNJGWMMfa9VA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-json-schema-draft-4": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", @@ -2136,294 +2160,329 @@ } }, "node_modules/@swagger-api/apidom-ns-openapi-3-1": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-1.0.0-alpha.5.tgz", - "integrity": "sha512-8VkdZ2MfxXIdmzQZrV0qGk18MG7XNJKIL3GT9lad9NyXyiKSvBVFJDmS4S43qcQTL0rjHXF6ds25yErDSTprjg==", - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^1.0.0-alpha.5", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-json-pointer": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-alpha.5", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-openapi-3-1/-/apidom-ns-openapi-3-1-1.0.0-beta.42.tgz", + "integrity": "sha512-yH1oXWLHnfj6OfrxY8ukWBtz5Dbb+63bS1Cq1JJ1ekToqsBc2A7rVpZ9x8sAD/Ht7x+UPGYJ0waOIgo4++MKGw==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.0.0-beta.42", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-json-pointer": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-json-schema-2020-12": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", "ts-mixer": "^6.0.3" } }, - "node_modules/@swagger-api/apidom-ns-workflows-1": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-ns-workflows-1/-/apidom-ns-workflows-1-1.0.0-alpha.5.tgz", - "integrity": "sha512-6cMv37y4kftJySoMAeubz5yhHaRKnSK0YglvCv8v7rE2OBduR/yEITDOej2/KFAnt29LxkhotSbNsmHx0weICQ==", + "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-1.0.0-beta.42.tgz", + "integrity": "sha512-Oezvh7FPKk/XZsxPdO7b9sXLH0L0DiTz36wJ3pql5JVkWlsOer1d12YrbkYTqmNgG8vtqbRjmadrbglW9mF/yw==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-api-design-systems": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", - "ramda-adjunct": "^5.0.0", - "ts-mixer": "^6.0.3" + "ramda-adjunct": "^5.0.0" } }, - "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-json": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-json/-/apidom-parser-adapter-api-design-systems-json-1.0.0-alpha.5.tgz", - "integrity": "sha512-QVWS2sPKA1sG52UIJut/St6+j7zO8QxzPlL5akR/8QPX2FWKqmw808Ewvjq9WLtqlPhVY2G33tv90d4/FJUNwQ==", + "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-1.0.0-beta.42.tgz", + "integrity": "sha512-FuO/CN+VBD4O3tL084+ETF1n/0Z7YLNVggDWTKwe9nbYCl4XFFAE8OEzy0ZuxcRw22RpXV5j+FSzhuGbGOxRfw==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-api-design-systems": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-api-design-systems": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, - "node_modules/@swagger-api/apidom-parser-adapter-api-design-systems-yaml": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-api-design-systems-yaml/-/apidom-parser-adapter-api-design-systems-yaml-1.0.0-alpha.5.tgz", - "integrity": "sha512-T7UD/SWd5u2zlPyswDdtfAStm6Qt5hQWAWvCmQKxy37qJA9QGXcQKNavaSMPGvN660hufNaJEBxgJ/B0Zd5iaw==", + "node_modules/@swagger-api/apidom-parser-adapter-arazzo-json-1": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-arazzo-json-1/-/apidom-parser-adapter-arazzo-json-1-1.0.0-beta.42.tgz", + "integrity": "sha512-oBxP3dpqzBNTuS6busCX7zmbe3D4fZle8C2Npl+v0F1aEVqkNP0oN/j/chaKzurfCQGsrS9Jr9mwPi745orSFw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.42", + "@types/ramda": "~0.30.0", + "ramda": "~0.30.0", + "ramda-adjunct": "^5.0.0" + } + }, + "node_modules/@swagger-api/apidom-parser-adapter-arazzo-yaml-1": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-arazzo-yaml-1/-/apidom-parser-adapter-arazzo-yaml-1-1.0.0-beta.42.tgz", + "integrity": "sha512-wnfaoK0UlVEjoeKcvMVJcKEo5WtJ5YkGBbv+8kPMow5lAPdkXmYDUSSqA/P9ASdxa4jYA89jnPJWUemeBskwAA==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-api-design-systems": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-json-2": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-1.0.0-alpha.5.tgz", - "integrity": "sha512-UfCS9DFIURTUfaHfmEn8omHaevIV2i24Ncp46M/Pnk6JwZHjAEMxmPxsgMl4TTGbzqvySUQsJka8Qz1ziYZ1og==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-json-2/-/apidom-parser-adapter-asyncapi-json-2-1.0.0-beta.42.tgz", + "integrity": "sha512-hEYunAYPAIi6rj+qoS9DFgAwMI3HZiSn55LGE5namgHYt65OlLBy1iOlIu5boRbzY+4tekd1ukS1gpCLrPReNQ==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-1.0.0-alpha.5.tgz", - "integrity": "sha512-X5avFyLnlu6Zjyul35f8Ff0DRE70aNc+Bk7il+eV8g+FR/qgrmuNziQEBOhCrIUnYB1kFbTty6BZRsNLdjW9XQ==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-asyncapi-yaml-2/-/apidom-parser-adapter-asyncapi-yaml-2-1.0.0-beta.42.tgz", + "integrity": "sha512-U9JWILb4gszyacGBXeHkzOKBALlyMR68EnvTyGJt+v4OjOi+c8jC/TqEhrBD8/5toW0htFwPnwyiMSQhs503oQ==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-json": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-1.0.0-alpha.5.tgz", - "integrity": "sha512-NdVjlRrtr1EvrBsk6DHSkjI8zdnSve/bjeGgo0NR2IRmA/8BRcY6rffM1BR76Ku+CjxhCB2mfQxotilD71dL+g==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-json/-/apidom-parser-adapter-json-1.0.0-beta.42.tgz", + "integrity": "sha512-uoYEbaeRJKr6bSCBCxKidXQ4tvZguPAksZ1mTZ8hDCBUILQ+6DX3GBHATSk3pf5LpGFf/RMD2T/Hrh7qqqY8jw==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^1.0.0-alpha.5", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-error": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.0.0-beta.42", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0", - "tree-sitter": "=0.20.4", - "tree-sitter-json": "=0.20.2", - "web-tree-sitter": "=0.20.3" + "tree-sitter": "=0.21.1", + "tree-sitter-json": "=0.24.8", + "web-tree-sitter": "=0.24.5" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-2": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-2/-/apidom-parser-adapter-openapi-json-2-1.0.0-alpha.5.tgz", - "integrity": "sha512-qOwQl2WezfdDVmtf9ZlOiqT1hcDS52j7ZbBdH9MqMGJ+/mo6sv0qEY2ZXS104lWeRamgi4o/4o4jGqjZS1YrMg==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-2/-/apidom-parser-adapter-openapi-json-2-1.0.0-beta.42.tgz", + "integrity": "sha512-dxPRV0Swz0iyQtH/jj8lqj4tYNEy/baLoEPIMujzxD8Zezk4q9RtUqiGCvyXFV2lpnuh0dyC0MaxyBgC7/S9QA==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-openapi-2": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-0": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-1.0.0-alpha.5.tgz", - "integrity": "sha512-t5oj7XteTu2Yh8uNkzXAcKU81CQky+q6Qt/ImQ/S6MGxpXJnWwgVfm/j/dH2wnHFKghNS3vgm6IewpojSbUw4w==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-0/-/apidom-parser-adapter-openapi-json-3-0-1.0.0-beta.42.tgz", + "integrity": "sha512-5VbcsK0hY8sRg/1WZihHdRno1Q5D7lP8xObzQ9gxKAsUOWDmm+o5Uqk2odgg5YZHKa1bONJ+h2MUDceK4Pgwuw==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-json-3-1": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-1.0.0-alpha.5.tgz", - "integrity": "sha512-w0G53HXYdzcespfa3atN90jVLDRoH9FU7XEWG4DvFWM90WGwuNscojcaB28r8pZMhSQAKMPxggh6PnmvK3gdEQ==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-json-3-1/-/apidom-parser-adapter-openapi-json-3-1-1.0.0-beta.42.tgz", + "integrity": "sha512-G1b9GLImFoJ9O9GGiWW46ypSA2vMuDLc/HE+IJcoqPgtmT/lPJ/uzxOsckPxH3bcooaWNrjooh2S0+brpYJO+w==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-2": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-2/-/apidom-parser-adapter-openapi-yaml-2-1.0.0-alpha.5.tgz", - "integrity": "sha512-nfeYRL0o6QwtKsyF30d2JmtW7fzoI/EYKSFgzaDm7IFlrQWMpB6BidpZKdk5MtYN4zvmfAM+lOhrqR7a5BvHMg==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-2/-/apidom-parser-adapter-openapi-yaml-2-1.0.0-beta.42.tgz", + "integrity": "sha512-UCogu7Jm87DvZO+luXiOy/2ddmfpwoOWqAALbdUmwTielXVGoB+X2QdH9vVJddI21SNQu9+/8yLKXS3yj8TjGQ==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-openapi-2": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-1.0.0-alpha.5.tgz", - "integrity": "sha512-HRziGD/YUcO21hmDIYNzwYivp/faeZRxcq8Gex7RLLhJZ60fGTJJ1k1yhWFPNSe9DEJUNBN949SDxMdZnGT9PQ==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-0/-/apidom-parser-adapter-openapi-yaml-3-0-1.0.0-beta.42.tgz", + "integrity": "sha512-S5U5F0XymP4bPJbhc6G4SILVt186LOJsNBPRyuJba3XTq14eombgSF8ZG6VHb2AJiYp9W+/SEL1HgUJKUjkocg==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, "node_modules/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-1.0.0-alpha.5.tgz", - "integrity": "sha512-aul2wSOvkdp9jQjSv1pvEGllVaDUnTKmRbCy7M/dFQyIhJQBvwW+/Cu//PprzAODtFNraOBjIXiJ5tVdv6NuIQ==", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-openapi-yaml-3-1/-/apidom-parser-adapter-openapi-yaml-3-1-1.0.0-beta.42.tgz", + "integrity": "sha512-hRFRFaRvfgeu1KLLnFxIGnnl3hh512l8dnnMUGgCQ1uB0JXsVZOEmPpuYVReij+dX6aK088ofXKZS19LV2gfXg==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.42", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" } }, - "node_modules/@swagger-api/apidom-parser-adapter-workflows-json-1": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-workflows-json-1/-/apidom-parser-adapter-workflows-json-1-1.0.0-alpha.5.tgz", - "integrity": "sha512-R1LVe/gx7fRSCuDmmN3qScWonz6Xlaw11J+NAfiJzrNXBy1Qa1mCxgGs47w0slQN+FjYkVj5Y/q29jJgpUbLHA==", + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2": { + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-1.0.0-beta.42.tgz", + "integrity": "sha512-8SlXky5RWaHI/QoZIt0ILakcpuCtatXpVGxqCStiweOBlu9aCe0Kaj6TQVI5ZDMvYzUtrSTdm/8WRYHQlrV2hA==", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-workflows-1": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-alpha.5", + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-ast": "^1.0.0-beta.42", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", + "@tree-sitter-grammars/tree-sitter-yaml": "=0.7.1", "@types/ramda": "~0.30.0", "ramda": "~0.30.0", - "ramda-adjunct": "^5.0.0" + "ramda-adjunct": "^5.0.0", + "tree-sitter": "=0.22.4", + "web-tree-sitter": "=0.24.5" } }, - "node_modules/@swagger-api/apidom-parser-adapter-workflows-yaml-1": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-workflows-yaml-1/-/apidom-parser-adapter-workflows-yaml-1-1.0.0-alpha.5.tgz", - "integrity": "sha512-W5wD+TdGNdW4aP9uqkxFbVmjWvLOXyV02VvyStyTlzxdUaPzKY3FGaxjxk8TGVRqwe2yEQVUc2zfGalrScA/Sg==", + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/@tree-sitter-grammars/tree-sitter-yaml": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@tree-sitter-grammars/tree-sitter-yaml/-/tree-sitter-yaml-0.7.1.tgz", + "integrity": "sha512-AynBwkIoQCTgjDR33bDUp9Mqq+YTco0is3n5hRApMqG9of/6A4eQsfC1/uSEeHSUyMQSYawcAWamsexnVpIP4Q==", + "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-ns-workflows-1": "^1.0.0-alpha.5", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-alpha.5", - "@types/ramda": "~0.30.0", - "ramda": "~0.30.0", - "ramda-adjunct": "^5.0.0" + "node-addon-api": "^8.3.1", + "node-gyp-build": "^4.8.4" + }, + "peerDependencies": { + "tree-sitter": "^0.22.4" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } } }, - "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-parser-adapter-yaml-1-2/-/apidom-parser-adapter-yaml-1-2-1.0.0-alpha.5.tgz", - "integrity": "sha512-21TIQPkB+Z4ekNj5dh1uN0dhOBBCPeK572YpooA/pBTFLeH6Wtildx7ZZYfpJEejHaQKaqoRx3hp0G42GDOb7g==", + "node_modules/@swagger-api/apidom-parser-adapter-yaml-1-2/node_modules/tree-sitter": { + "version": "0.22.4", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.22.4.tgz", + "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", + "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-ast": "^1.0.0-alpha.5", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", - "@swagger-api/apidom-error": "^1.0.0-alpha.5", - "@types/ramda": "~0.30.0", - "ramda": "~0.30.0", - "ramda-adjunct": "^5.0.0", - "tree-sitter": "=0.20.4", - "tree-sitter-yaml": "=0.5.0", - "web-tree-sitter": "=0.20.3" + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" } }, "node_modules/@swagger-api/apidom-reference": { - "version": "1.0.0-alpha.5", - "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.0.0-alpha.5.tgz", - "integrity": "sha512-zPMTScWI8oVUAT//RdAhl9GJuwtQLibP8iCrqFQDGjBzKQS5Uxz4hSXr/jqKPdkCJXbEoP94yYjvQjtI5yrv1A==", - "dependencies": { - "@babel/runtime-corejs3": "^7.20.7", - "@swagger-api/apidom-core": "^1.0.0-alpha.5", + "version": "1.0.0-beta.42", + "resolved": "https://registry.npmjs.org/@swagger-api/apidom-reference/-/apidom-reference-1.0.0-beta.42.tgz", + "integrity": "sha512-C21LaCPOCAEq99UFXT2+NMEDsScAOWSUd1hMTkzXbGZPM4ZKIxC0MhExdU4uoXaswspRuTC99zT6XJZmYjz19w==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime-corejs3": "^7.26.10", + "@swagger-api/apidom-core": "^1.0.0-beta.42", + "@swagger-api/apidom-error": "^1.0.0-beta.42", "@types/ramda": "~0.30.0", - "axios": "^1.4.0", + "axios": "^1.9.0", "minimatch": "^7.4.3", "process": "^0.11.10", "ramda": "~0.30.0", "ramda-adjunct": "^5.0.0" }, "optionalDependencies": { - "@swagger-api/apidom-error": "^1.0.0-alpha.1", - "@swagger-api/apidom-json-pointer": "^1.0.0-alpha.1", - "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-alpha.1", - "@swagger-api/apidom-ns-openapi-2": "^1.0.0-alpha.1", - "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-alpha.1", - "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-alpha.1", - "@swagger-api/apidom-ns-workflows-1": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-json": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-openapi-json-2": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-workflows-json-1": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-workflows-yaml-1": "^1.0.0-alpha.1", - "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-alpha.1" + "@swagger-api/apidom-json-pointer": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-arazzo-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-asyncapi-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-3-0": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-3-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-api-design-systems-json": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-api-design-systems-yaml": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-arazzo-json-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-arazzo-yaml-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-asyncapi-json-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-asyncapi-yaml-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-json": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-json-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-json-3-0": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-json-3-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-2": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-0": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-openapi-yaml-3-1": "^1.0.0-beta.40 <1.0.0-rc.0", + "@swagger-api/apidom-parser-adapter-yaml-1-2": "^1.0.0-beta.40 <1.0.0-rc.0" } }, "node_modules/@swagger-api/apidom-reference/node_modules/minimatch": { "version": "7.4.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz", "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -2434,116 +2493,144 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "node_modules/@swaggerexpert/cookie": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@swaggerexpert/cookie/-/cookie-2.0.2.tgz", + "integrity": "sha512-DPI8YJ0Vznk4CT+ekn3rcFNq1uQwvUHZhH6WvTSPD0YKBIlMS9ur2RYKghXuxxOiqOam/i4lHJH4xTIiTgs3Mg==", + "license": "Apache-2.0", + "dependencies": { + "apg-lite": "^1.0.3" + }, "engines": { - "node": ">= 10" + "node": ">=12.20.0" + } + }, + "node_modules/@swaggerexpert/json-pointer": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@swaggerexpert/json-pointer/-/json-pointer-2.10.2.tgz", + "integrity": "sha512-qMx1nOrzoB+PF+pzb26Q4Tc2sOlrx9Ba2UBNX9hB31Omrq+QoZ2Gly0KLrQWw4Of1AQ4J9lnD+XOdwOdcdXqqw==", + "license": "Apache-2.0", + "dependencies": { + "apg-lite": "^1.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.9.tgz", + "integrity": "sha512-SPWC8kwG/dWBf7Py7cfheAPOxuvIv4fFQ54PdmYbg7CpXfsKxkucak43Q0qKsxVthhUJQ1A7CIMAIplq4BjVwA==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.9" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.9", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.9.tgz", + "integrity": "sha512-3jztt0jpaoJO5TARe2WIHC1UQC3VMLAFUW5mmMo0yrkwtDB2AQP0+sh10BVUpWrnvHjSLvzFizydtEGLCJKFoQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@types/codemirror": { - "version": "5.60.10", - "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.10.tgz", - "integrity": "sha512-ZTA3teiCWKT8HUUofqlGPlShu5ojdIajizsS0HpH6GL0/iEdjRt7fXbCLHHqKYP5k7dC/HnnWIjZAiELUwBdjQ==", + "version": "5.60.16", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.16.tgz", + "integrity": "sha512-V/yHdamffSS075jit+fDxaOAmdP2liok8NSNJnAZfDJErzOheuygHZEhAJrfmk5TEyM32MhkZjwo/idX791yxw==", + "license": "MIT", "dependencies": { "@types/tern": "*" } }, "node_modules/@types/estree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", - "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==" + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" }, "node_modules/@types/hast": { "version": "2.3.10", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", "dependencies": { "@types/unist": "^2" } }, "node_modules/@types/hoist-non-react-statics": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", - "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz", + "integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==", + "license": "MIT", "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" } }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } + "node_modules/@types/lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-QjCOmf5kYwekcsfEKhcEHMK8/SvgnneuSDXFERBuC/DPca2KJIO/gpChTsVb35BoWLBpEAJWz1GFVEArSdtKtw==", + "license": "MIT" }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "node_modules/@types/node": { + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", + "license": "MIT", "dependencies": { - "@types/istanbul-lib-report": "*" + "undici-types": "~6.21.0" } }, - "node_modules/@types/jsdom": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.1.tgz", - "integrity": "sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==", + "node_modules/@types/ramda": { + "version": "0.30.2", + "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.30.2.tgz", + "integrity": "sha512-PyzHvjCalm2BRYjAU6nIB3TprYwMNOUY/7P/N8bSzp9W/yM2YrtGtAnnVtaCNSeOZ8DzKyFDvaqQs7LnWwwmBA==", + "license": "MIT", "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", - "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" - }, - "node_modules/@types/lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-QjCOmf5kYwekcsfEKhcEHMK8/SvgnneuSDXFERBuC/DPca2KJIO/gpChTsVb35BoWLBpEAJWz1GFVEArSdtKtw==" - }, - "node_modules/@types/node": { - "version": "20.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.0.tgz", - "integrity": "sha512-najjVq5KN2vsH2U/xyh2opaSEz6cZMR2SetLIlxlj08nOcmPOemJmUK2o4kUzfLqfrWE0PIrNeE16XhYDd3nqg==" - }, - "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" - }, - "node_modules/@types/ramda": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@types/ramda/-/ramda-0.30.0.tgz", - "integrity": "sha512-DQtfqUbSB18iM9NHbQ++kVUDuBWHMr6T2FpW1XTiksYRGjq4WnNPZLt712OEHEBJs7aMyJ68Mf2kGMOP1srVVw==", - "dependencies": { - "types-ramda": "^0.30.0" + "types-ramda": "^0.30.1" } }, "node_modules/@types/react": { - "version": "18.3.3", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", - "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", + "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", + "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-redux": { - "version": "7.1.26", - "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.26.tgz", - "integrity": "sha512-UKPo7Cm7rswYU6PH6CmTNCRv5NYF3HrgKuHEYTK8g/3czYLrUux50gQ2pkxc9c7ZpQZi+PNhgmI8oNIRoiVIxg==", + "version": "7.1.34", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz", + "integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==", + "license": "MIT", "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -2551,70 +2638,61 @@ "redux": "^4.0.0" } }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" - }, "node_modules/@types/stylis": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz", "integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==", + "license": "MIT", "peer": true }, "node_modules/@types/tern": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.5.tgz", - "integrity": "sha512-POau56wDk3TQ0mQ0qG7XDzv96U5whSENZ9lC0htDvEH+9YUREo+J2U+apWcVRgR2UydEE70JXZo44goG+akTNQ==", + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "license": "MIT", "dependencies": { "@types/estree": "*" } }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true }, "node_modules/@types/unist": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz", - "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==" + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" }, "node_modules/@types/use-sync-external-store": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", - "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==" + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.5", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", - "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" - }, "node_modules/@whatwg-node/events": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.3.tgz", - "integrity": "sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA==" + "integrity": "sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA==", + "license": "MIT" }, "node_modules/@whatwg-node/fetch": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.8.tgz", "integrity": "sha512-CdcjGC2vdKhc13KKxgsc6/616BQ7ooDIgPeTuAiE8qfCnS0mGzcfCOoZXypQSz73nxI+GWc7ZReIAVhxoE1KCg==", + "license": "MIT", "dependencies": { "@peculiar/webcrypto": "^1.4.0", "@whatwg-node/node-fetch": "^0.3.6", @@ -2627,6 +2705,7 @@ "version": "0.3.6", "resolved": "https://registry.npmjs.org/@whatwg-node/node-fetch/-/node-fetch-0.3.6.tgz", "integrity": "sha512-w9wKgDO4C95qnXZRwZTfCmLWqyRnooGjcIwG0wADWjw9/HN0p7dtvtgSvItZtUyNteEvgTrd8QojNEqV6DAGTA==", + "license": "MIT", "dependencies": { "@whatwg-node/events": "^0.0.3", "busboy": "^1.6.0", @@ -2639,6 +2718,7 @@ "version": "0.1.11", "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz", "integrity": "sha512-mwEVBDUVODlsQQ5dfuLUS5/Tf7jqUKyhKYHmVi4fPB6bDMOfWvUPJmKgS1Z7Za/sOI3vzWt4+O7yCiL/70MogA==", + "license": "MIT", "dependencies": { "tslib": "^1.9.3" } @@ -2646,18 +2726,14 @@ "node_modules/@wry/equality/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "deprecated": "Use your platform's native atob() and btoa() methods instead" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" @@ -2667,9 +2743,10 @@ } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -2677,65 +2754,56 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-globals": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", - "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", - "dependencies": { - "acorn": "^8.1.0", - "acorn-walk": "^8.0.2" - } - }, "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "license": "MIT", "engines": { "node": ">=0.4.0" } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dependencies": { - "debug": "4" - }, + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "color-convert": "^1.9.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=4" } }, "node_modules/apg-lite": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/apg-lite/-/apg-lite-1.0.3.tgz", - "integrity": "sha512-lOoNkL7vN7PGdyQMFPey1aok2oVVqvs3n7UMFBRvQ9FoELSbKhgPc3rd7JptaGwCmo4125gLX9Cqb8ElvLCFaQ==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/apg-lite/-/apg-lite-1.0.5.tgz", + "integrity": "sha512-SlI+nLMQDzCZfS39ihzjGp3JNBQfJXyMi6cg9tkLOCPVErgFsUIAEdO9IezR7kbP5Xd0ozcPNQBkf9TO5cHgWw==", + "license": "BSD-2-Clause" }, "node_modules/apollo-link": { "version": "1.2.14", "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.14.tgz", "integrity": "sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==", + "license": "MIT", "dependencies": { "apollo-utilities": "^1.3.0", "ts-invariant": "^0.4.0", @@ -2750,6 +2818,7 @@ "version": "1.5.17", "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.17.tgz", "integrity": "sha512-uWcqAotbwDEU/9+Dm9e1/clO7hTB2kQ/94JYcGouBVLjoKmTeJTUPQKcJGpPwUjZcSqgYicbFqQSoJIW0yrFvg==", + "license": "MIT", "dependencies": { "apollo-link": "^1.2.14", "apollo-link-http-common": "^0.2.16", @@ -2763,6 +2832,7 @@ "version": "0.2.16", "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz", "integrity": "sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==", + "license": "MIT", "dependencies": { "apollo-link": "^1.2.14", "ts-invariant": "^0.4.0", @@ -2775,17 +2845,20 @@ "node_modules/apollo-link-http-common/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/apollo-link-http/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/apollo-link-ws": { "version": "1.0.20", "resolved": "https://registry.npmjs.org/apollo-link-ws/-/apollo-link-ws-1.0.20.tgz", "integrity": "sha512-mjSFPlQxmoLArpHBeUb2Xj+2HDYeTaJqFGOqQ+I8NVJxgL9lJe84PDWcPah/yMLv3rB7QgBDSuZ0xoRFBPlySw==", + "license": "MIT", "dependencies": { "apollo-link": "^1.2.14", "tslib": "^1.9.3" @@ -2797,17 +2870,20 @@ "node_modules/apollo-link-ws/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/apollo-link/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/apollo-utilities": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.4.tgz", "integrity": "sha512-pk2hiWrCXMAy2fRPwEyhvka+mqwzeP60Jr1tRYi5xru+3ko94HI9o6lK0CT33/w4RDlxWchmdhDCrvdr+pHCig==", + "license": "MIT", "dependencies": { "@wry/equality": "^0.1.2", "fast-json-stable-stringify": "^2.0.0", @@ -2821,20 +2897,23 @@ "node_modules/apollo-utilities/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/aria-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.3.tgz", - "integrity": "sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -2845,24 +2924,27 @@ "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/asn1js": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", - "integrity": "sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "license": "BSD-3-Clause", "dependencies": { - "pvtsutils": "^1.3.2", + "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", - "tslib": "^2.4.0" + "tslib": "^2.8.1" }, "engines": { "node": ">=12.0.0" @@ -2871,25 +2953,29 @@ "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/autolinker": { "version": "3.16.2", "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-3.16.2.tgz", "integrity": "sha512-JiYl7j2Z19F9NdTmirENSUUIIL/9MytEWtmzhfmsKPCp9E+G35Y0UNCMoM9tFigxT59qSc8Ml2dlZXOCVTYwuA==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2900,6 +2986,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz", "integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==", + "license": "MIT", "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", @@ -2915,6 +3002,7 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==", + "license": "MIT", "dependencies": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -2925,22 +3013,26 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", - "hasInstallScript": true + "hasInstallScript": true, + "license": "MIT" }, "node_modules/babel-runtime/node_modules/regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", + "license": "MIT" }, "node_modules/backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==" + "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==", + "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/base64-js": { "version": "1.5.1", @@ -2959,12 +3051,14 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/bfj": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.2.tgz", "integrity": "sha512-BmBJa4Lip6BPRINSZ0BPEIfB1wUY/9rwbwvIHQA1KjX9om29B6id0wnWXq7m3bn5JrUVjeOTnVuhPT1FiHwPGw==", + "license": "MIT", "dependencies": { "bluebird": "^3.5.5", "check-types": "^8.0.3", @@ -2979,6 +3073,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/biskviit/-/biskviit-1.0.1.tgz", "integrity": "sha512-VGCXdHbdbpEkFgtjkeoBN8vRlbj1ZRX2/mxhE8asCCRalUx2nBzOomLJv8Aw/nRt5+ccDb+tPKidg4XxcfGW4w==", + "license": "MIT", "dependencies": { "psl": "^1.1.7" }, @@ -2986,37 +3081,28 @@ "node": ">=1.0.0" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "optional": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", + "qs": "6.13.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -3029,6 +3115,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -3036,27 +3123,15 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/boom": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/boom/-/boom-7.3.0.tgz", "integrity": "sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A==", "deprecated": "This module has moved and is now available at @hapi/boom. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.", + "license": "BSD-3-Clause", "dependencies": { "hoek": "6.x.x" } @@ -3065,16 +3140,18 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3083,12 +3160,13 @@ "node_modules/browser-fingerprint": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/browser-fingerprint/-/browser-fingerprint-0.0.1.tgz", - "integrity": "sha512-b8SXP7yOlzLUJXF8WUvIjmbJzkJC0X6OHe7J9a/SHqEBC7a9Eglag6AANSTJz82h5U582kuxm/5TPudnD68EPA==" + "integrity": "sha512-b8SXP7yOlzLUJXF8WUvIjmbJzkJC0X6OHe7J9a/SHqEBC7a9Eglag6AANSTJz82h5U582kuxm/5TPudnD68EPA==", + "license": "MIT" }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", "funding": [ { "type": "opencollective", @@ -3103,12 +3181,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -3117,30 +3196,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "optional": true, - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -3156,6 +3211,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3163,18 +3219,48 @@ "node_modules/calculate-size": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/calculate-size/-/calculate-size-1.1.1.tgz", - "integrity": "sha512-jJZ7pvbQVM/Ss3VO789qpsypN3xmnepg242cejOAslsmlZLYw2dnj7knnNowabQ0Kzabzx56KFTy2Pot/y6FmA==" + "integrity": "sha512-jJZ7pvbQVM/Ss3VO789qpsypN3xmnepg242cejOAslsmlZLYw2dnj7knnNowabQ0Kzabzx56KFTy2Pot/y6FmA==", + "license": "MIT" }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -3186,12 +3272,14 @@ "node_modules/call-me-maybe": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } @@ -3200,14 +3288,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001625", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001625.tgz", - "integrity": "sha512-4KE9N2gcRH+HQhpeiRZXd+1niLB/XNLAhSy4z7fI8EzcbcPoAqjNInxVHTiTwWfTIV4w096XG8OtCOCQQKPv3w==", + "version": "1.0.30001721", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", + "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", "funding": [ { "type": "opencollective", @@ -3222,27 +3311,28 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "CC-BY-4.0", "peer": true }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=4" } }, "node_modules/character-entities": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3252,6 +3342,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3261,6 +3352,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3269,42 +3361,20 @@ "node_modules/check-types": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/check-types/-/check-types-8.0.3.tgz", - "integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==" - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "optional": true - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } + "integrity": "sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==", + "license": "MIT" }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", - "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" - }, - "node_modules/client-only": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -3318,62 +3388,67 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/codemirror": { - "version": "5.65.15", - "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.15.tgz", - "integrity": "sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g==" + "version": "5.65.19", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.19.tgz", + "integrity": "sha512-+aFkvqhaAVr1gferNMuN8vkTSrWIFvzlMV9I2KBLCWS2WpZ2+UAkZjlMZmEuT+gcXTi6RrGQCkWq1/bDtGqhIA==", + "license": "MIT" }, "node_modules/codemirror-graphql": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-2.0.11.tgz", - "integrity": "sha512-j1QDDXKVkpin2VsyS0ke2nAhKal6/N1UJtgnBGrPe3gj9ZSP6/K8Xytft94k0xW6giIU/JhZjvW0GwwERNzbFA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-2.2.2.tgz", + "integrity": "sha512-9WY6YGPeXDLvdHeBNh4mompvZKapYJsfEXhodCW72W+9E/z8GajgCZjGLOaq57a9fD2f9+Zp/J0FGiypOtNgrw==", + "license": "MIT", "dependencies": { "@types/codemirror": "^0.0.90", - "graphql-language-service": "5.2.0" + "graphql-language-service": "5.4.0" }, "peerDependencies": { "@codemirror/language": "6.0.0", "codemirror": "^5.65.3", - "graphql": "^15.5.0 || ^16.0.0" + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" } }, "node_modules/codemirror-graphql/node_modules/@types/codemirror": { "version": "0.0.90", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-0.0.90.tgz", "integrity": "sha512-8Z9+tSg27NPRGubbUPUCrt5DDG/OWzLph5BvcDykwR5D7RyZh5mhHG0uS1ePKV1YFCA+/cwc4Ey2AJAEFfV3IA==", + "license": "MIT", "dependencies": { "@types/tern": "*" } }, "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "color-name": "1.1.3" } }, "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" }, "node_modules/colorette": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", - "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==" + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "license": "MIT" }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3385,6 +3460,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -3393,17 +3469,20 @@ "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -3415,6 +3494,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -3423,12 +3503,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT", "peer": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -3436,21 +3518,24 @@ "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" }, "node_modules/copy-to-clipboard": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", "dependencies": { "toggle-selection": "^1.0.6" } }, "node_modules/core-js": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", - "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", + "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", "hasInstallScript": true, + "license": "MIT", "peer": true, "funding": { "type": "opencollective", @@ -3458,10 +3543,11 @@ } }, "node_modules/core-js-pure": { - "version": "3.37.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.37.1.tgz", - "integrity": "sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", "hasInstallScript": true, + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -3471,6 +3557,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz", "integrity": "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==", + "license": "MIT", "dependencies": { "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -3484,12 +3571,14 @@ "node_modules/cosmiconfig/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/cosmiconfig/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3501,16 +3590,25 @@ "version": "15.7.0", "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz", "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==", + "license": "MIT", "dependencies": { "loose-envify": "^1.3.1", "object-assign": "^4.1.1" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT", + "peer": true + }, "node_modules/cryptiles": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-4.1.2.tgz", "integrity": "sha512-U2ALcoAHvA1oO2xOreyHvtkQ+IELqDG2WVWRI1GH/XEmmfGIOalnM5MU5Dd2ITyWfr3m6kNqXiy8XuYyd4wKJw==", "deprecated": "This version has been deprecated in accordance with the hapi support policy (hapi.im/support). Please upgrade to the latest version to get the best features, bug fixes, and security patches. If you are unable to upgrade at this time, paid support is available for older versions (hapi.im/commercial).", + "license": "BSD-3-Clause", "dependencies": { "boom": "7.x.x" }, @@ -3522,6 +3620,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", "engines": { "node": ">=4" } @@ -3530,6 +3629,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", "peer": true, "dependencies": { "camelize": "^1.0.0", @@ -3540,39 +3640,21 @@ "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==" - }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==" - }, - "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", - "dependencies": { - "cssom": "~0.3.6" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "license": "MIT" }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" }, "node_modules/cuid": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/cuid/-/cuid-1.3.8.tgz", "integrity": "sha512-MoL67ZZuBetDMxzrZtO+Iq1ATajFACQCP52QRinBgd3yTjYdv54mJO8ibUrh06fojKCoX5P2i7KkEatm4VTIOQ==", "deprecated": "Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead.", + "license": "MIT", "dependencies": { "browser-fingerprint": "0.0.1", "core-js": "^1.1.1", @@ -3583,32 +3665,28 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", "integrity": "sha512-ZiPp9pZlgxpWRu0M+YWbm6+aQ84XEfH1JRXvfOc/fILWI0VKhLC2LX13X1NYq4fULzLMq7Hfh43CSo2/aIaUPA==", - "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js." - }, - "node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", - "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" - }, - "engines": { - "node": ">=12" - } + "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.", + "license": "MIT" }, "node_modules/dataloader": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.2.tgz", - "integrity": "sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", + "license": "MIT" + }, + "node_modules/debounce-promise": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz", + "integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==", + "license": "MIT" }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3619,11 +3697,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" - }, "node_modules/decko": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decko/-/decko-1.2.0.tgz", @@ -3633,29 +3706,16 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", "engines": { "node": ">=0.10" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "optional": true, - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -3664,6 +3724,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3672,6 +3733,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -3688,6 +3750,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -3696,6 +3759,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3704,29 +3768,23 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", "engines": { "node": ">= 0.8", "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -3738,77 +3796,92 @@ "version": "3.4.0", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2" } }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "deprecated": "Use your platform's native DOMException instead", - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/dompurify": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.4.tgz", - "integrity": "sha512-2gnshi6OshmuKil8rMZuQCGiUF3cUxHY3NGDzUAdUx/NPEe5DVnO8BDoAQouvgwnx0R/+a6jUn36Z0FSdq8vww==" + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/drange": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/dset": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", - "integrity": "sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==", + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/ejs": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.7.4.tgz", "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", "hasInstallScript": true, + "license": "Apache-2.0", "engines": { "node": ">=0.10.0" } }, "node_modules/electron-to-chromium": { - "version": "1.4.787", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.787.tgz", - "integrity": "sha512-d0EFmtLPjctczO3LogReyM2pbBiiZbnsKnGF+cdZhsYzHm/A0GV7W94kqzLD8SN4O3f3iHlgLUChqghgyznvCQ==", + "version": "1.5.165", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz", + "integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==", + "license": "ISC", "peer": true }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3817,23 +3890,16 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", "integrity": "sha512-bl1LAgiQc4ZWr++pNYUdRe/alecaHFeHxIJ/pNciqGdKXghaTCOwKkbKp6ye7pKZGu/GcaSXFk8PBVhgs+dJdA==", + "license": "MIT", "dependencies": { "iconv-lite": "~0.4.13" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -3845,17 +3911,16 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -3864,6 +3929,34 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, "engines": { "node": ">= 0.4" } @@ -3871,12 +3964,14 @@ "node_modules/es6-promise": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -3884,40 +3979,23 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, "node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" + "node": ">=0.8.0" } }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -3926,26 +4004,11 @@ "node": ">=4" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -3953,53 +4016,47 @@ "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" }, "node_modules/exenv": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", - "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "optional": true, - "engines": { - "node": ">=6" - } + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==", + "license": "BSD-3-Clause" }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -4008,20 +4065,17 @@ }, "engines": { "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "engines": { - "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -4029,31 +4083,20 @@ "node_modules/express/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" - }, - "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/extract-files": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/extract-files/-/extract-files-11.0.0.tgz", "integrity": "sha512-FuoE1qtbJ4bBVvv94CC7s0oTnKUGvQs+Rjf1L2SJFfS+HTVVjhPFtehPdQ0JiGPqVNfSSZvL5yzHHQq2Z4WNhQ==", + "license": "MIT", "engines": { "node": "^12.20 || >= 14.13" }, @@ -4064,23 +4107,26 @@ "node_modules/fast-decode-uri-component": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", - "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -4089,17 +4135,20 @@ "node_modules/fast-json-patch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", - "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==", + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" }, "node_modules/fast-querystring": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", "dependencies": { "fast-decode-uri-component": "^1.0.1" } @@ -4107,12 +4156,14 @@ "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" }, "node_modules/fast-url-parser": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", + "license": "MIT", "dependencies": { "punycode": "^1.3.2" } @@ -4120,12 +4171,32 @@ "node_modules/fast-url-parser/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", + "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -4134,6 +4205,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "license": "MIT", "dependencies": { "format": "^0.2.0" }, @@ -4146,6 +4218,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-5O8TwrGzoNblBG/jtK4NFuZwNCkZX6s5GfRNOaGtm+QGJEuNakSC/i2RW0R93KX6E0jVjNXm6O3CRN4Ql3K+yA==", + "license": "MIT", "dependencies": { "biskviit": "1.0.1", "encoding": "0.1.12" @@ -4155,14 +4228,16 @@ "version": "3.6.1", "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", + "license": "BSD-3-Clause", "engines": { "node": ">= 0.4.0" } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4171,12 +4246,13 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -4191,6 +4267,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -4198,18 +4275,20 @@ "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -4222,15 +4301,19 @@ "node_modules/foreach": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==" + "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", + "license": "MIT" }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -4249,56 +4332,52 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/framer-motion": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-6.5.1.tgz", - "integrity": "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==", + "version": "12.16.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.16.0.tgz", + "integrity": "sha512-xryrmD4jSBQrS2IkMdcTmiS4aSKckbS7kLDCuhUn9110SQKG1w3zlq1RTqCblewg+ZYe+m3sdtzQA6cRwo5g8Q==", + "license": "MIT", "dependencies": { - "@motionone/dom": "10.12.0", - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "popmotion": "11.0.3", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" + "motion-dom": "^12.16.0", + "motion-utils": "^12.12.1", + "tslib": "^2.4.0" }, "peerDependencies": { - "react": ">=16.8 || ^17.0.0 || ^18.0.0", - "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" - } - }, - "node_modules/framesync": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/framesync/-/framesync-6.0.1.tgz", - "integrity": "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==", - "dependencies": { - "tslib": "^2.1.0" + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "optional": true - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4307,6 +4386,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", "peer": true, "engines": { "node": ">=6.9.0" @@ -4316,20 +4396,27 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4342,20 +4429,41 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "optional": true + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-value": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", + "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=6.0" + } }, "node_modules/glob-parent": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -4367,6 +4475,7 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "license": "MIT", "engines": { "node": ">=4" } @@ -4375,6 +4484,7 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -4391,41 +4501,39 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, "node_modules/graphiql": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-3.2.2.tgz", - "integrity": "sha512-Tpv9gz9/xfOCJq2RTU/ByPgCFkh3ftN16xmcJxNms3j7C0eJ9z7xg6J0lASGGJ6mTeIW9myEI98SJBPL1c4vcA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-4.1.1.tgz", + "integrity": "sha512-B0PD6YDG/999VF1luabIUfOSZT14a85JEVqhjWxF+k5kxDz7DPkp9DrpOhY3a0ot0iuWR03K+ESZo7wd5F5q6A==", + "license": "MIT", "dependencies": { - "@graphiql/react": "^0.22.1", - "@graphiql/toolkit": "^0.9.1", - "graphql-language-service": "^5.2.0", - "markdown-it": "^14.1.0" + "@graphiql/plugin-doc-explorer": "^0.2.2", + "@graphiql/plugin-history": "^0.2.2", + "@graphiql/react": "^0.34.1", + "react-compiler-runtime": "19.1.0-rc.1" }, "peerDependencies": { - "graphql": "^15.5.0 || ^16.0.0", - "react": "^16.8.0 || ^17 || ^18", - "react-dom": "^16.8.0 || ^17 || ^18" + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" } }, "node_modules/graphql": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz", - "integrity": "sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==", + "version": "15.10.1", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.10.1.tgz", + "integrity": "sha512-BL/Xd/T9baO6NFzoMpiMD7YUZ62R6viR5tp/MULVEnbYJXZA//kRNW7J0j1w/wXArgL0sCxhDfK5dczSKn3+cg==", + "license": "MIT", "engines": { "node": ">= 10.x" } @@ -4434,6 +4542,7 @@ "version": "4.5.0", "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-4.5.0.tgz", "integrity": "sha512-x6D0/cftpLUJ0Ch1e5sj1TZn6Wcxx4oMfmhaG9shM0DKajA9iR+j1z86GSTQ19fShbGvrSSvbIQsHku6aQ6BBw==", + "license": "MIT", "dependencies": { "@graphql-tools/graphql-file-loader": "^7.3.7", "@graphql-tools/json-file-loader": "^7.3.7", @@ -4464,6 +4573,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4473,6 +4583,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.3.tgz", "integrity": "sha512-lIUdtK5hdofgCTu3aT0sOaHsYR37viUuIc0rwnnDXImbwFRcumyLMeZaM0t0I/fgxS6s6JMfu0rLD1Wz9pv1ng==", + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -4481,10 +4592,12 @@ } }, "node_modules/graphql-language-service": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.2.0.tgz", - "integrity": "sha512-o/ZgTS0pBxWm3hSF4+6GwiV1//DxzoLWEbS38+jqpzzy1d/QXBidwQuVYTOksclbtOJZ3KR/tZ8fi/tI6VpVMg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/graphql-language-service/-/graphql-language-service-5.4.0.tgz", + "integrity": "sha512-g4N5PKh4Dxow9zuHrzX6PHuWWL/aQPYgzZvZst1KkWYFW1H1rmOA/p0/eEJ2WVuoCCfy1tyAR91iG92MAKCILA==", + "license": "MIT", "dependencies": { + "debounce-promise": "^3.1.2", "nullthrows": "^1.0.0", "vscode-languageserver-types": "^3.17.1" }, @@ -4492,7 +4605,7 @@ "graphql": "dist/temp-bin.js" }, "peerDependencies": { - "graphql": "^15.5.0 || ^16.0.0" + "graphql": "^15.5.0 || ^16.0.0 || ^17.0.0" } }, "node_modules/graphql-language-service-interface": { @@ -4500,6 +4613,7 @@ "resolved": "https://registry.npmjs.org/graphql-language-service-interface/-/graphql-language-service-interface-2.10.2.tgz", "integrity": "sha512-RKIEBPhRMWdXY3fxRs99XysTDnEgAvNbu8ov/5iOlnkZsWQNzitjtd0O0l1CutQOQt3iXoHde7w8uhCnKL4tcg==", "deprecated": "this package has been merged into graphql-language-service", + "license": "MIT", "dependencies": { "graphql-config": "^4.1.0", "graphql-language-service-parser": "^1.10.4", @@ -4516,6 +4630,7 @@ "resolved": "https://registry.npmjs.org/graphql-language-service-parser/-/graphql-language-service-parser-1.10.4.tgz", "integrity": "sha512-duDE+0aeKLFVrb9Kf28U84ZEHhHcvTjWIT6dJbIAQJWBaDoht0D4BK9EIhd94I3DtKRc1JCJb2+70y1lvP/hiA==", "deprecated": "this package has been merged into graphql-language-service", + "license": "MIT", "dependencies": { "graphql-language-service-types": "^1.8.7" }, @@ -4528,6 +4643,7 @@ "resolved": "https://registry.npmjs.org/graphql-language-service-types/-/graphql-language-service-types-1.8.7.tgz", "integrity": "sha512-LP/Mx0nFBshYEyD0Ny6EVGfacJAGVx+qXtlJP4hLzUdBNOGimfDNtMVIdZANBXHXcM41MDgMHTnyEx2g6/Ttbw==", "deprecated": "this package has been merged into graphql-language-service", + "license": "MIT", "dependencies": { "graphql-config": "^4.1.0", "vscode-languageserver-types": "^3.15.1" @@ -4541,6 +4657,7 @@ "resolved": "https://registry.npmjs.org/graphql-language-service-utils/-/graphql-language-service-utils-2.7.1.tgz", "integrity": "sha512-Wci5MbrQj+6d7rfvbORrA9uDlfMysBWYaG49ST5TKylNaXYFf3ixFOa74iM1KtM9eidosUbI3E1JlWi0JaidJA==", "deprecated": "this package has been merged into graphql-language-service", + "license": "MIT", "dependencies": { "@types/json-schema": "7.0.9", "graphql-language-service-types": "^1.8.7", @@ -4553,23 +4670,26 @@ "node_modules/graphql-language-service-utils/node_modules/@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==" + "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", + "license": "MIT" }, "node_modules/graphql-playground-react": { - "version": "1.7.26", - "resolved": "https://registry.npmjs.org/graphql-playground-react/-/graphql-playground-react-1.7.26.tgz", - "integrity": "sha512-OtJZ8DXota2F/fKXfwrO0sTAg+i7XiTy6CLqoFvG36ZEx0rdpUsl3rJgreQig0dGNUpc2Zn0qu2qa+IJgOxnxw==", + "version": "1.7.28", + "resolved": "https://registry.npmjs.org/graphql-playground-react/-/graphql-playground-react-1.7.28.tgz", + "integrity": "sha512-YPR33Ph9kuKlFOMpc8mKGsL/fOiX0ltgzV1BokXYNsBLtTVQnhdCSKQuZg80CcxWKD8syNuWXDkGZSAdFkwmRg==", + "license": "MIT", "dependencies": { "@types/lru-cache": "^4.1.1", "apollo-link": "^1.2.13", "apollo-link-http": "^1.5.16", "apollo-link-ws": "^1.0.19", "calculate-size": "^1.1.1", - "codemirror": "^5.52.2", - "codemirror-graphql": "^0.12.0-alpha.8", + "codemirror": "^5.58.1", + "codemirror-graphql": "^0.12.3", "copy-to-clipboard": "^3.0.8", "cryptiles": "4.1.2", "cuid": "^1.3.8", + "escape-html": "^1.0.3", "graphiql": "^0.17.5", "graphql": "^15.3.0", "immutable": "^4.0.0-rc.9", @@ -4579,8 +4699,8 @@ "keycode": "^2.1.9", "lodash": "^4.17.11", "lodash.debounce": "^4.0.8", - "markdown-it": "^8.4.1", - "marked": "^0.8.2", + "lru-cache": "^6.0.0", + "markdown-it": "^12.2.0", "prettier": "2.0.2", "prop-types": "^15.7.2", "query-string": "5", @@ -4593,7 +4713,7 @@ "react-helmet": "^5.2.0", "react-input-autosize": "^2.2.1", "react-modal": "^3.1.11", - "react-redux": "^7.2.0", + "react-redux": "^7.2.1", "react-router-dom": "^4.2.2", "react-sortable-hoc": "^0.8.3", "react-transition-group": "^2.2.1", @@ -4614,15 +4734,32 @@ "zen-observable": "^0.7.1" } }, + "node_modules/graphql-playground-react/node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/graphql-playground-react/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "license": "MIT" + }, "node_modules/graphql-playground-react/node_modules/@emotion/unitless": { "version": "0.7.5", "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" }, "node_modules/graphql-playground-react/node_modules/codemirror-graphql": { "version": "0.12.4", "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-0.12.4.tgz", "integrity": "sha512-gWxmLk2OzPVzvwAXO0K52MtU1n6ylMNbKp0LtZHioK0NEUwLnSL5iPKVXn8MgvYqS8Yos/CG5WrP9Y7RWTO4mg==", + "license": "MIT", "dependencies": { "graphql-language-service-interface": "^2.4.3", "graphql-language-service-parser": "^1.6.5" @@ -4636,16 +4773,28 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-2.3.2.tgz", "integrity": "sha512-VOFaeZA053BqvvvqIA8c9n0+9vFppVBAHCp6JgFTtTMU3Mzi+XnelJ9XC9ul3BqFzZyQ5N+H0SnwsWT2Ebchxw==", + "license": "MIT", "dependencies": { "camelize": "^1.0.0", "css-color-keywords": "^1.0.0", "postcss-value-parser": "^3.3.0" } }, + "node_modules/graphql-playground-react/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "extraneous": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/graphql-playground-react/node_modules/graphiql": { "version": "0.17.5", "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-0.17.5.tgz", "integrity": "sha512-ogNsrg9qM1py9PzcIUn+C29JukOADbjIfB6zwtfui4BrpOEpDb5UZ6TjAmSL/F/8tCt4TbgwKtkSrBeLNNUrqA==", + "license": "MIT", "dependencies": { "codemirror": "^5.47.0", "codemirror-graphql": "^0.11.6", @@ -4665,6 +4814,7 @@ "version": "0.11.6", "resolved": "https://registry.npmjs.org/codemirror-graphql/-/codemirror-graphql-0.11.6.tgz", "integrity": "sha512-/zVKgOVS2/hfjAY0yoBkLz9ESHnWKBWpBNXQSoFF4Hl5q5AS2DmM22coonWKJcCvNry6TLak2F+QkzPeKVv3Eg==", + "license": "MIT", "dependencies": { "graphql-language-service-interface": "^2.3.3", "graphql-language-service-parser": "^1.5.2" @@ -4677,12 +4827,23 @@ "node_modules/graphql-playground-react/node_modules/graphiql/node_modules/entities": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" + "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", + "license": "BSD-2-Clause" + }, + "node_modules/graphql-playground-react/node_modules/graphiql/node_modules/linkify-it": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", + "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } }, "node_modules/graphql-playground-react/node_modules/graphiql/node_modules/markdown-it": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "entities": "~2.0.0", @@ -4694,30 +4855,24 @@ "markdown-it": "bin/markdown-it.js" } }, - "node_modules/graphql-playground-react/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, "node_modules/graphql-playground-react/node_modules/linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "license": "MIT", "dependencies": { "uc.micro": "^1.0.1" } }, "node_modules/graphql-playground-react/node_modules/markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" }, @@ -4725,25 +4880,38 @@ "markdown-it": "bin/markdown-it.js" } }, + "node_modules/graphql-playground-react/node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/graphql-playground-react/node_modules/markdown-it/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/graphql-playground-react/node_modules/mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" }, "node_modules/graphql-playground-react/node_modules/postcss-value-parser": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==" + "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", + "license": "MIT" }, "node_modules/graphql-playground-react/node_modules/react": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -4757,6 +4925,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/react-codemirror/-/react-codemirror-1.0.0.tgz", "integrity": "sha512-pPvL8b1vwLyfX5f3EMLyqZVXYY/qAKdqURYxi3izYfjWbnUdqVaFBA7z78o9eEM+UzgxuKjI864BJkPIRVS2JA==", + "license": "MIT", "dependencies": { "classnames": "^2.2.5", "codemirror": "^5.18.2", @@ -4774,6 +4943,7 @@ "version": "16.14.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -4788,6 +4958,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.2.tgz", "integrity": "sha512-jQJgYCA3S0j+cuOwzuCd1OjmBmnZLdqQdiLKRYrsMMzbjUrVDS5RvJUDwJqA7sKuksDuzFtm6hZGKFu7Mjk5aw==", + "license": "MIT", "dependencies": { "prop-types": "^15.5.8" }, @@ -4799,6 +4970,7 @@ "version": "0.8.4", "resolved": "https://registry.npmjs.org/react-sortable-hoc/-/react-sortable-hoc-0.8.4.tgz", "integrity": "sha512-J9AFEQAJ7u2YWdVzkU5E3ewrG82xQ4xF1ZPrZYKliDwlVBDkmjri+iKFAEt6NCDIRiBZ4hiN5vzI8pwy/dGPHw==", + "license": "MIT", "dependencies": { "babel-runtime": "^6.11.6", "invariant": "^2.2.1", @@ -4809,15 +4981,11 @@ "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0" } }, - "node_modules/graphql-playground-react/node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" - }, "node_modules/graphql-playground-react/node_modules/scheduler": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -4828,6 +4996,7 @@ "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-4.4.1.tgz", "integrity": "sha512-RNqj14kYzw++6Sr38n7197xG33ipEOktGElty4I70IKzQF1jzaD1U4xQ+Ny/i03UUhHlC5NWEO+d8olRCDji6g==", "hasInstallScript": true, + "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.0.0", "@babel/traverse": "^7.0.0", @@ -4851,36 +5020,29 @@ "node_modules/graphql-playground-react/node_modules/stylis": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/stylis/-/stylis-3.5.4.tgz", - "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==" + "integrity": "sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==", + "license": "MIT" }, "node_modules/graphql-playground-react/node_modules/stylis-rule-sheet": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", + "license": "MIT", "peerDependencies": { "stylis": "^3.5.0" } }, - "node_modules/graphql-playground-react/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/graphql-playground-react/node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" }, "node_modules/graphql-ws": { "version": "5.12.1", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.1.tgz", "integrity": "sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -4892,6 +5054,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "license": "MIT", "dependencies": { "duplexer": "^0.1.1", "pify": "^4.0.1" @@ -4901,17 +5064,19 @@ } }, "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -4919,10 +5084,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4930,10 +5096,14 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, "engines": { "node": ">= 0.4" }, @@ -4945,6 +5115,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -4956,6 +5127,7 @@ "version": "2.2.5", "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/unified" @@ -4965,6 +5137,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "license": "MIT", "dependencies": { "@types/hast": "^2.0.0", "comma-separated-tokens": "^1.0.0", @@ -4977,23 +5150,26 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/hey-listen": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "license": "BSD-3-Clause", "engines": { "node": "*" } }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", + "license": "CC0-1.0" + }, "node_modules/history": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.1.2", "loose-envify": "^1.2.0", @@ -5007,12 +5183,14 @@ "version": "6.1.3", "resolved": "https://registry.npmjs.org/hoek/-/hoek-6.1.3.tgz", "integrity": "sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ==", - "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues." + "deprecated": "This module has moved and is now available at @hapi/hoek. Please update your dependencies as this version is no longer maintained an may contain bugs and security issues.", + "license": "BSD-3-Clause" }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } @@ -5021,25 +5199,16 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "license": "MIT", "engines": { "node": ">= 6.0.0" } }, - "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", - "dependencies": { - "whatwg-encoding": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -5051,40 +5220,30 @@ "node": ">= 0.8" } }, - "node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/http2-client": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", - "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==" + "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", + "license": "MIT" }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -5109,25 +5268,29 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/immutable": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz", - "integrity": "sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==" + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "license": "MIT" }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -5143,6 +5306,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { "node": ">=4" } @@ -5150,18 +5314,14 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "optional": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } @@ -5170,6 +5330,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -5178,6 +5339,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5187,6 +5349,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "license": "MIT", "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" @@ -5199,12 +5362,14 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-decimal": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5214,6 +5379,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5222,6 +5388,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "engines": { "node": ">=8" } @@ -5230,6 +5397,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -5241,6 +5409,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -5250,6 +5419,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -5258,6 +5428,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -5265,15 +5436,11 @@ "node": ">=0.10.0" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" - }, "node_modules/is-primitive": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", "integrity": "sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5282,6 +5449,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5289,17 +5457,20 @@ "node_modules/is-what": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", - "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==" + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "license": "MIT" }, "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5308,6 +5479,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", "integrity": "sha512-9c4TNAKYXM5PRyVcwUZrF3W09nQ+sO7+jydgs4ZGW9dhsLG2VOlISJABombdQqQRXCwuYG3sYV/puGf5rp0qmA==", + "license": "MIT", "dependencies": { "node-fetch": "^1.0.1", "whatwg-fetch": ">=0.10.0" @@ -5317,6 +5489,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "license": "MIT", "peerDependencies": { "ws": "*" } @@ -5324,86 +5497,14 @@ "node_modules/iterall": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz", - "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==" - }, - "node_modules/jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-mock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } + "integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==", + "license": "MIT" }, "node_modules/jiti": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.17.1.tgz", "integrity": "sha512-NZIITw8uZQFuzQimqjUxIrIcEdxYDFIe/0xYfIlVXTkiBjjyBEvgasj5bb0/cHtPRD/NziPbT312sFrkI5ALpw==", + "license": "MIT", "bin": { "jiti": "bin/jiti.js" } @@ -5411,12 +5512,14 @@ "node_modules/js-file-download": { "version": "0.4.12", "resolved": "https://registry.npmjs.org/js-file-download/-/js-file-download-0.4.12.tgz", - "integrity": "sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==" + "integrity": "sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==", + "license": "MIT" }, "node_modules/js-levenshtein": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5424,12 +5527,14 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { "version": "3.14.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -5438,70 +5543,29 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsdom": { - "version": "20.0.3", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", - "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", - "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.8.1", - "acorn-globals": "^7.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.4.2", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.2", - "parse5": "^7.1.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.1.2", - "w3c-xmlserializer": "^4.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.11.0", - "xml-name-validator": "^4.0.0" + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" }, "engines": { - "node": ">=14" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" }, "node_modules/json-pointer": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.2.tgz", "integrity": "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw==", + "license": "MIT", "dependencies": { "foreach": "^2.0.4" } @@ -5509,14 +5573,23 @@ "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" }, "node_modules/json-stable-stringify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.2.tgz", - "integrity": "sha512-eunSSaEnxV12z+Z73y/j5N37/In40GK4GmsSy+tEHJMxknvqnA7/djeYtAgW0GsWHUfg+847WJjKaEylk2y09g==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.3.0.tgz", + "integrity": "sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==", + "license": "MIT", "dependencies": { - "jsonify": "^0.0.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "isarray": "^2.0.5", + "jsonify": "^0.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5526,6 +5599,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", "peer": true, "bin": { "json5": "lib/cli.js" @@ -5538,6 +5612,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.1.tgz", "integrity": "sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==", + "license": "Public Domain", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5545,22 +5620,26 @@ "node_modules/just-curry-it": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/just-curry-it/-/just-curry-it-3.2.1.tgz", - "integrity": "sha512-Q8206k8pTY7krW32cdmPsP+DqqLgWx/hYPSj9/+7SYqSqz7UuwPbfSe07lQtvuuaVyiSJveXk0E5RydOuWwsEg==" + "integrity": "sha512-Q8206k8pTY7krW32cdmPsP+DqqLgWx/hYPSj9/+7SYqSqz7UuwPbfSe07lQtvuuaVyiSJveXk0E5RydOuWwsEg==", + "license": "MIT" }, "node_modules/keycode": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz", - "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==" + "integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==", + "license": "MIT" }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" } @@ -5568,27 +5647,33 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash._getnative": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==" + "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==", + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -5600,6 +5685,7 @@ "version": "1.20.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "license": "MIT", "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" @@ -5610,28 +5696,34 @@ } }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "peer": true, + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", "dependencies": { - "yallist": "^3.0.2" + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", - "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==" + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "license": "MIT" }, "node_modules/mark.js": { "version": "8.11.1", "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", - "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==" + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "license": "MIT" }, "node_modules/markdown-it": { "version": "14.1.0", "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -5647,28 +5739,41 @@ "node_modules/markdown-it/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/marked": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.8.2.tgz", - "integrity": "sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "license": "MIT", "bin": { - "marked": "bin/marked" + "marked": "bin/marked.js" }, "engines": { - "node": ">= 8.16.2" + "node": ">= 12" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" } }, "node_modules/mdurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==" + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5676,25 +5781,32 @@ "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" }, "node_modules/merge-anything": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-2.4.4.tgz", "integrity": "sha512-l5XlriUDJKQT12bH+rVhAHjwIuXWdAIecGwsYjv2LJo+dA1AeRTmeQS+3QBpO6lEthBMDi2IUMpLC1yyRvGlwQ==", + "license": "MIT", "dependencies": { "is-what": "^3.3.1" } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", "engines": { "node": ">= 8" } @@ -5703,6 +5815,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/meros/-/meros-1.3.0.tgz", "integrity": "sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w==", + "license": "MIT", "engines": { "node": ">=13" }, @@ -5719,16 +5832,18 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -5739,6 +5854,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -5750,6 +5866,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5758,6 +5875,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -5765,22 +5883,11 @@ "node": ">= 0.6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "optional": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minim": { "version": "0.23.8", "resolved": "https://registry.npmjs.org/minim/-/minim-0.23.8.tgz", "integrity": "sha512-bjdr2xW1dBCMsMGGsUeqM4eFI60m94+szhxWys+B1ztIt6gWSfeGBdSVCIawezeHYLYn0j6zrsXdQS/JllBzww==", + "license": "MIT", "dependencies": { "lodash": "^4.15.0" }, @@ -5792,6 +5899,7 @@ "version": "5.1.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -5803,6 +5911,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5811,6 +5920,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -5818,16 +5928,11 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "optional": true - }, "node_modules/mobx": { - "version": "6.12.3", - "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.12.3.tgz", - "integrity": "sha512-c8NKkO4R2lShkSXZ2Ongj1ycjugjzFFo/UswHBnS62y07DMcTc9Rvo03/3nRyszIvwPNljlkd4S828zIBv/piw==", + "version": "6.13.7", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz", + "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", + "license": "MIT", "peer": true, "funding": { "type": "opencollective", @@ -5835,19 +5940,20 @@ } }, "node_modules/mobx-react": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-7.6.0.tgz", - "integrity": "sha512-+HQUNuh7AoQ9ZnU6c4rvbiVVl+wEkb9WqYsVDzGLng+Dqj1XntHu79PvEWKtSMoMj67vFp/ZPXcElosuJO8ckA==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-9.2.0.tgz", + "integrity": "sha512-dkGWCx+S0/1mfiuFfHRH8D9cplmwhxOV5CkXMp38u6rQGG2Pv3FWYztS0M7ncR6TyPRQKaTG/pnitInoYE9Vrw==", + "license": "MIT", "dependencies": { - "mobx-react-lite": "^3.4.0" + "mobx-react-lite": "^4.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" }, "peerDependencies": { - "mobx": "^6.1.0", - "react": "^16.8.0 || ^17 || ^18" + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" }, "peerDependenciesMeta": { "react-dom": { @@ -5859,16 +5965,20 @@ } }, "node_modules/mobx-react-lite": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.4.3.tgz", - "integrity": "sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.1.0.tgz", + "integrity": "sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.4.0" + }, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" }, "peerDependencies": { - "mobx": "^6.1.0", - "react": "^16.8.0 || ^17 || ^18" + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" }, "peerDependenciesMeta": { "react-dom": { @@ -5879,27 +5989,38 @@ } } }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "node_modules/motion-dom": { + "version": "12.16.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.16.0.tgz", + "integrity": "sha512-Z2nGwWrrdH4egLEtgYMCEN4V2qQt1qxlKy/uV7w691ztyA41Q5Rbn0KNGbsNVDZr9E8PD2IOQ3hSccRnB6xWzw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.12.1" + } }, - "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", - "optional": true + "node_modules/motion-utils": { + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.12.1.tgz", + "integrity": "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -5908,41 +6029,45 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", - "optional": true - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/node-abi": { - "version": "3.63.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.63.0.tgz", - "integrity": "sha512-vAszCsOUrUxjGAmdnM/pq7gUgie0IRteCQMX6d4A534fQCR93EJU5qgzBvU6EkFfK27s0T3HEV3BOyJIr7OMYw==", - "optional": true, - "dependencies": { - "semver": "^7.3.5" - }, + "node_modules/neotraverse": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 10" } }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", - "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" + "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^18 || ^20 || >= 21" + } }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", "funding": [ { "type": "github", @@ -5953,6 +6078,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "engines": { "node": ">=10.5.0" } @@ -5961,6 +6087,7 @@ "version": "1.7.3", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", + "license": "MIT", "dependencies": { "encoding": "^0.1.11", "is-stream": "^1.0.1" @@ -5970,6 +6097,7 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/node-fetch-commonjs/-/node-fetch-commonjs-3.3.2.tgz", "integrity": "sha512-VBlAiynj3VMLrotgwOS3OyECFxas5y7ltLcK4t41lMUZeaK15Ym4QRkqN0EQKAFL42q9i21EPKjzLUPfltR72A==", + "license": "MIT", "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -5986,6 +6114,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz", "integrity": "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg==", + "license": "MIT", "dependencies": { "http2-client": "^1.2.5" }, @@ -5996,12 +6125,26 @@ "node_modules/node-fingerprint": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/node-fingerprint/-/node-fingerprint-0.0.2.tgz", - "integrity": "sha512-vPFfTD5EBJieQ4SI3v61fWxlV1kav3m9Dbejd6CjWhOJn8s+XMxpOOosCNAyIrUQ/jJOlPndfrZ0lSw4+RgwcA==" + "integrity": "sha512-vPFfTD5EBJieQ4SI3v61fWxlV1kav3m9Dbejd6CjWhOJn8s+XMxpOOosCNAyIrUQ/jJOlPndfrZ0lSw4+RgwcA==", + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } }, "node_modules/node-readfiles": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", + "license": "MIT", "dependencies": { "es6-promise": "^3.2.1" } @@ -6009,18 +6152,21 @@ "node_modules/node-readfiles/node_modules/es6-promise": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", - "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==" + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "license": "MIT", "peer": true }, "node_modules/normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", + "license": "MIT", "dependencies": { "remove-trailing-separator": "^1.0.1" }, @@ -6031,17 +6177,14 @@ "node_modules/nullthrows": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", - "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" - }, - "node_modules/nwsapi": { - "version": "2.2.10", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz", - "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==" + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT" }, "node_modules/oas-kit-common": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/oas-kit-common/-/oas-kit-common-1.0.8.tgz", "integrity": "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ==", + "license": "BSD-3-Clause", "dependencies": { "fast-safe-stringify": "^2.0.7" } @@ -6050,6 +6193,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/oas-linter/-/oas-linter-3.2.2.tgz", "integrity": "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ==", + "license": "BSD-3-Clause", "dependencies": { "@exodus/schemasafe": "^1.0.0-rc.2", "should": "^13.2.1", @@ -6063,6 +6207,7 @@ "version": "2.5.6", "resolved": "https://registry.npmjs.org/oas-resolver/-/oas-resolver-2.5.6.tgz", "integrity": "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ==", + "license": "BSD-3-Clause", "dependencies": { "node-fetch-h2": "^2.3.0", "oas-kit-common": "^1.0.8", @@ -6081,6 +6226,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz", "integrity": "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ==", + "license": "BSD-3-Clause", "funding": { "url": "https://github.com/Mermade/oas-kit?sponsor=1" } @@ -6089,6 +6235,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/oas-validator/-/oas-validator-5.0.8.tgz", "integrity": "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw==", + "license": "BSD-3-Clause", "dependencies": { "call-me-maybe": "^1.0.1", "oas-kit-common": "^1.0.8", @@ -6107,22 +6254,37 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -6130,39 +6292,46 @@ "node": ">= 0.8" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "optional": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/openapi-path-templating": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/openapi-path-templating/-/openapi-path-templating-1.5.1.tgz", - "integrity": "sha512-kgRHToVP571U1YzUnaZnWaUIygon2itg5g96kwaFIi8bnpsw4oXYOk7k59Ivn+ley1iQnMENe/1HSovpPVZuXA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/openapi-path-templating/-/openapi-path-templating-2.2.1.tgz", + "integrity": "sha512-eN14VrDvl/YyGxxrkGOHkVkWEoPyhyeydOUrbvjoz8K5eIGgELASwN1eqFOJ2CTQMGCy2EntOK1KdtJ8ZMekcg==", + "license": "Apache-2.0", "dependencies": { - "apg-lite": "^1.0.3" + "apg-lite": "^1.0.4" }, "engines": { "node": ">=12.20.0" } }, "node_modules/openapi-sampler": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.5.1.tgz", - "integrity": "sha512-tIWIrZUKNAsbqf3bd9U1oH6JEXo8LNYuDlXw26By67EygpjT+ArFnsxxyTMjFWRfbqo5ozkvgSQDK69Gd8CddA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/openapi-sampler/-/openapi-sampler-1.6.1.tgz", + "integrity": "sha512-s1cIatOqrrhSj2tmJ4abFYZQK6l5v+V4toO5q1Pa0DyN8mtyqy2I+Qrj5W9vOELEtybIMQs/TBZGVO/DtTFK8w==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.7", + "fast-xml-parser": "^4.5.0", "json-pointer": "0.6.2" } }, + "node_modules/openapi-server-url-templating": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/openapi-server-url-templating/-/openapi-server-url-templating-1.3.0.tgz", + "integrity": "sha512-DPlCms3KKEbjVQb0spV6Awfn6UWNheuG/+folQPzh/wUaKwuqvj8zt5gagD7qoyxtE03cIiKPgLFS3Q8Bz00uQ==", + "license": "Apache-2.0", + "dependencies": { + "apg-lite": "^1.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/opener": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "license": "(WTFPL OR MIT)", "bin": { "opener": "bin/opener-bin.js" } @@ -6171,6 +6340,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6185,6 +6355,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -6196,6 +6367,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "license": "MIT", "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", @@ -6213,6 +6385,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -6226,21 +6399,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse5": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", - "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", - "dependencies": { - "entities": "^4.4.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -6248,38 +6411,50 @@ "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", + "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", + "license": "MIT", "dependencies": { "isarray": "0.0.1" } }, + "node_modules/path-to-regexp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "license": "MIT" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/perfect-scrollbar": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.5.tgz", - "integrity": "sha512-dzalfutyP3e/FOpdlhVryN4AJ5XDVauVWxybSkLZmakFE2sS3y3pc4JnSprw8tGmHvkaG5Edr5T7LBTZ+WWU2g==" + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/perfect-scrollbar/-/perfect-scrollbar-1.5.6.tgz", + "integrity": "sha512-rixgxw3SxyJbCaSpo1n35A/fwI1r2rdwMKOTCg/AcG+xOEyZcE8UHVjpZMFCVImzsFoCZeJTT+M/rdEIQYO2nw==", + "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -6291,6 +6466,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", "engines": { "node": ">=6" } @@ -6299,6 +6475,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "license": "MIT", "engines": { "node": ">=4" } @@ -6307,6 +6484,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", "integrity": "sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.17.8" }, @@ -6314,21 +6492,10 @@ "node": ">=10" } }, - "node_modules/popmotion": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-11.0.3.tgz", - "integrity": "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==", - "dependencies": { - "framesync": "6.0.1", - "hey-listen": "^1.0.8", - "style-value-types": "5.0.0", - "tslib": "^2.1.0" - } - }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -6343,11 +6510,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -6357,38 +6525,14 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT", "peer": true }, - "node_modules/prebuild-install": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", - "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prettier": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.2.tgz", "integrity": "sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==", + "license": "MIT", "bin": { "prettier": "bin-prettier.js" }, @@ -6396,39 +6540,11 @@ "node": ">=10.13.0" } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" - }, "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", "engines": { "node": ">=6" } @@ -6437,6 +6553,7 @@ "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", "engines": { "node": ">= 0.6.0" } @@ -6445,6 +6562,7 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -6455,6 +6573,7 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "license": "MIT", "dependencies": { "xtend": "^4.0.0" }, @@ -6467,6 +6586,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -6478,27 +6598,26 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "optional": true, + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" } }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -6507,30 +6626,34 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/pvtsutils": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.5.tgz", - "integrity": "sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==", + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", "dependencies": { - "tslib": "^2.6.1" + "tslib": "^2.8.1" } }, "node_modules/pvutils": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/qs": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", - "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" }, @@ -6545,6 +6668,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "license": "MIT", "dependencies": { "decode-uri-component": "^0.2.0", "object-assign": "^4.1.0", @@ -6557,7 +6681,8 @@ "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -6576,21 +6701,24 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/ramda": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.0.tgz", - "integrity": "sha512-13Y0iMhIQuAm/wNGBL/9HEqIfRGmNmjKnTPlKWfA9f7dnDkr8d45wQ+S7+ZLh/Pq9PdcGxkqKUEA7ySu1QSd9Q==", + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", + "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/ramda" } }, "node_modules/ramda-adjunct": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-5.0.0.tgz", - "integrity": "sha512-iEehjqp/ZGjYZybZByDaDu27c+79SE7rKDcySLdmjAwKWkz6jNhvGgZwzUGaMsij8Llp9+1N1Gy0drpAq8ZSyA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ramda-adjunct/-/ramda-adjunct-5.1.0.tgz", + "integrity": "sha512-8qCpl2vZBXEJyNbi4zqcgdfHtcdsWjOGbiNSEnEBrM6Y0OKOT8UxJbIVGm1TIcjaSu2MxaWcgtsNlKlCk7o7qg==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.3" }, @@ -6606,6 +6734,7 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "license": "MIT", "dependencies": { "drange": "^1.0.2", "ret": "^0.2.0" @@ -6618,6 +6747,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -6626,14 +6756,16 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -6644,25 +6776,11 @@ "node": ">= 0.8" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "optional": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" }, @@ -6674,14 +6792,25 @@ "version": "15.6.3", "resolved": "https://registry.npmjs.org/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.3.tgz", "integrity": "sha512-EDJbgKTtGRLhr3wiGDXK/+AEJ59yqGS+tKE6mue0aNXT6ZMR7VJbbzIiT6akotmHg1BLj46ElJSb+NBMp80XBg==", + "license": "MIT", "dependencies": { "object-assign": "^4.1.0" } }, + "node_modules/react-compiler-runtime": { + "version": "19.1.0-rc.1", + "resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-19.1.0-rc.1.tgz", + "integrity": "sha512-wCt6g+cRh8g32QT18/9blfQHywGjYu+4FlEc3CW1mx3pPxYzZZl1y+VtqxRgnKKBCFLIGUYxog4j4rs5YS86hw==", + "license": "MIT", + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental" + } + }, "node_modules/react-copy-to-clipboard": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "license": "MIT", "dependencies": { "copy-to-clipboard": "^3.3.1", "prop-types": "^15.8.1" @@ -6694,6 +6823,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/react-debounce-input/-/react-debounce-input-3.3.0.tgz", "integrity": "sha512-VEqkvs8JvY/IIZvh71Z0TC+mdbxERvYF33RcebnodlsUZ8RSgyKe2VWaHXv4+/8aoOgXLxWrdsYs2hDhcwbUgA==", + "license": "MIT", "dependencies": { "lodash.debounce": "^4", "prop-types": "^15.8.1" @@ -6705,12 +6835,14 @@ "node_modules/react-display-name": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.5.tgz", - "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==" + "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==", + "license": "MIT" }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6722,12 +6854,14 @@ "node_modules/react-fast-compare": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", - "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==", + "license": "MIT" }, "node_modules/react-helmet": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz", "integrity": "sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA==", + "license": "MIT", "dependencies": { "object-assign": "^4.1.1", "prop-types": "^15.5.4", @@ -6742,6 +6876,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.2.0.tgz", "integrity": "sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w==", + "license": "MIT", "dependencies": { "shallowequal": "^1.0.1" }, @@ -6753,6 +6888,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/react-immutable-proptypes/-/react-immutable-proptypes-2.2.0.tgz", "integrity": "sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==", + "license": "MIT", "dependencies": { "invariant": "^2.2.2" }, @@ -6764,6 +6900,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-immutable-pure-component/-/react-immutable-pure-component-2.2.2.tgz", "integrity": "sha512-vkgoMJUDqHZfXXnjVlG3keCxSO/U6WeDQ5/Sl0GK2cH8TOxEzQ5jXqDXHEL/jqk6fsNxV05oH5kD7VNMUE2k+A==", + "license": "MIT", "peerDependencies": { "immutable": ">= 2 || >= 4.0.0-rc", "react": ">= 16.6", @@ -6774,6 +6911,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.2.tgz", "integrity": "sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==", + "license": "MIT", "peerDependencies": { "react": "^16.8.4 || ^17.0.0 || ^18.0.0" } @@ -6781,35 +6919,36 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", + "license": "MIT" }, "node_modules/react-modal": { - "version": "3.16.1", - "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", - "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "version": "3.16.3", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.3.tgz", + "integrity": "sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==", + "license": "MIT", "dependencies": { "exenv": "^1.2.0", "prop-types": "^15.7.2", "react-lifecycles-compat": "^3.0.0", "warning": "^4.0.3" }, - "engines": { - "node": ">=8" - }, "peerDependencies": { - "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", - "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19" } }, "node_modules/react-redux": { "version": "7.2.9", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -6833,25 +6972,27 @@ "node_modules/react-redux/node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "license": "MIT" }, "node_modules/react-remove-scroll": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz", - "integrity": "sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", "dependencies": { - "react-remove-scroll-bar": "^2.3.3", - "react-style-singleton": "^2.2.1", + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" }, "engines": { "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -6860,19 +7001,20 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", "dependencies": { - "react-style-singleton": "^2.2.1", + "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -6884,6 +7026,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-4.3.1.tgz", "integrity": "sha512-yrvL8AogDh2X42Dt9iknk4wF4V8bWREPirFfS9gLU1huk6qK41sg7Z/1S81jjTrGHxa3B8R3J6xIkDAA6CVarg==", + "license": "MIT", "dependencies": { "history": "^4.7.2", "hoist-non-react-statics": "^2.5.0", @@ -6901,6 +7044,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-4.3.1.tgz", "integrity": "sha512-c/MlywfxDdCp7EnB7YfPMOfMD3tOtIjrQlj/CKfNMBxdmpJP8xcz5P/UAFn3JbnQCNUxsHyVVqllF9LhgVyFCA==", + "license": "MIT", "dependencies": { "history": "^4.7.2", "invariant": "^2.2.4", @@ -6916,23 +7060,24 @@ "node_modules/react-router/node_modules/hoist-non-react-statics": { "version": "2.5.5", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz", - "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==" + "integrity": "sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==", + "license": "BSD-3-Clause" }, "node_modules/react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", "dependencies": { "get-nonce": "^1.0.0", - "invariant": "^2.2.4", "tslib": "^2.0.0" }, "engines": { "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -6941,12 +7086,14 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz", - "integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==", + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", "prismjs": "^1.27.0", "refractor": "^3.6.0" @@ -6956,21 +7103,32 @@ } }, "node_modules/react-tabs": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-4.3.0.tgz", - "integrity": "sha512-2GfoG+f41kiBIIyd3gF+/GRCCYtamC8/2zlAcD8cqQmqI9Q+YVz7fJLHMmU9pXDVYYHpJeCgUSBJju85vu5q8Q==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-tabs/-/react-tabs-6.1.0.tgz", + "integrity": "sha512-6QtbTRDKM+jA/MZTTefvigNxo0zz+gnBTVFw2CFVvq+f2BuH0nF0vDLNClL045nuTAdOoK/IL1vTP0ZLX0DAyQ==", + "license": "MIT", "dependencies": { - "clsx": "^1.1.0", + "clsx": "^2.0.0", "prop-types": "^15.5.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-0 || ^18.0.0" + "react": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-tabs/node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" } }, "node_modules/react-transition-group": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", "integrity": "sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==", + "license": "BSD-3-Clause", "dependencies": { "dom-helpers": "^3.4.0", "loose-envify": "^1.4.0", @@ -6983,9 +7141,10 @@ } }, "node_modules/react-virtualized": { - "version": "9.22.5", - "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.5.tgz", - "integrity": "sha512-YqQMRzlVANBv1L/7r63OHa2b0ZsAaDp1UhVNEdUaXI8A5u6hTpA5NYtUueLH2rFuY/27mTGIBl7ZhqFKzw18YQ==", + "version": "9.22.6", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.6.tgz", + "integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.7.2", "clsx": "^1.0.4", @@ -6995,56 +7154,43 @@ "react-lifecycles-compat": "^3.0.4" }, "peerDependencies": { - "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", - "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + "react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-virtualized/node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "optional": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/redoc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.1.4.tgz", - "integrity": "sha512-wqStZ9oPDTCmp2DMLqecxvxjltX4Bi2xfJiQq3+5Ty1g9Au7TGpJi9PPs42x7p/FxG8t+HlJ6xA7pXpWLZ+UyQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/redoc/-/redoc-2.5.0.tgz", + "integrity": "sha512-NpYsOZ1PD9qFdjbLVBZJWptqE+4Y6TkUuvEOqPUmoH7AKOmPcE+hYjotLxQNTqVoWL4z0T2uxILmcc8JGDci+Q==", + "license": "MIT", "dependencies": { "@redocly/openapi-core": "^1.4.0", "classnames": "^2.3.2", "decko": "^1.2.0", - "dompurify": "^3.0.6", + "dompurify": "^3.2.4", "eventemitter3": "^5.0.1", - "jest-environment-jsdom": "^29.7.0", "json-pointer": "^0.6.2", "lunr": "^2.3.9", "mark.js": "^8.11.1", "marked": "^4.3.0", - "mobx-react": "^7.2.0", + "mobx-react": "^9.1.1", "openapi-sampler": "^1.5.0", "path-browserify": "^1.0.1", "perfect-scrollbar": "^1.5.5", "polished": "^4.2.2", "prismjs": "^1.29.0", "prop-types": "^15.8.1", - "react-tabs": "^4.3.0", + "react-tabs": "^6.0.2", "slugify": "~1.4.7", "stickyfill": "^1.1.1", "swagger2openapi": "^7.0.8", @@ -7057,31 +7203,22 @@ "peerDependencies": { "core-js": "^3.1.4", "mobx": "^6.0.4", - "react": "^16.8.4 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0", + "react": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.4 || ^17.0.0 || ^18.0.0 || ^19.0.0", "styled-components": "^4.1.1 || ^5.1.1 || ^6.0.5" } }, - "node_modules/redoc/node_modules/marked": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 12" - } - }, "node_modules/reduce-reducers": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/reduce-reducers/-/reduce-reducers-0.4.3.tgz", - "integrity": "sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw==" + "integrity": "sha512-+CNMnI8QhgVMtAt54uQs3kUxC3Sybpa7Y63HR14uGLgI9/QR5ggHvpxwhGGe3wmx5V91YwqQIblN9k5lspAmGw==", + "license": "MIT" }, "node_modules/redux": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "license": "MIT", "dependencies": { "@babel/runtime": "^7.9.2" } @@ -7090,6 +7227,7 @@ "version": "2.6.5", "resolved": "https://registry.npmjs.org/redux-actions/-/redux-actions-2.6.5.tgz", "integrity": "sha512-pFhEcWFTYNk7DhQgxMGnbsB1H2glqhQJRQrtPb96kD3hWiZRzXHwwmFPswg6V2MjraXRXWNmuP9P84tvdLAJmw==", + "license": "MIT", "dependencies": { "invariant": "^2.2.4", "just-curry-it": "^3.1.0", @@ -7102,6 +7240,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/redux-immutable/-/redux-immutable-4.0.0.tgz", "integrity": "sha512-SchSn/DWfGb3oAejd+1hhHx01xUoxY+V7TeK0BKqpkLKiQPVFf7DYzEaKmrEVxsWxielKfSK9/Xq66YyxgR1cg==", + "license": "BSD-3-Clause", "peerDependencies": { "immutable": "^3.8.1 || ^4.0.0-rc.1" } @@ -7109,12 +7248,14 @@ "node_modules/redux-localstorage": { "version": "1.0.0-rc5", "resolved": "https://registry.npmjs.org/redux-localstorage/-/redux-localstorage-1.0.0-rc5.tgz", - "integrity": "sha512-7Vv82DGrsb3ncDJxpkEStVoT+qgI9UdrRc5Pl/l6rWsq4j1hQCyG7U+tiOsposeWgSRuqMQRyIe9scR8eED5tA==" + "integrity": "sha512-7Vv82DGrsb3ncDJxpkEStVoT+qgI9UdrRc5Pl/l6rWsq4j1hQCyG7U+tiOsposeWgSRuqMQRyIe9scR8eED5tA==", + "license": "MIT" }, "node_modules/redux-localstorage-debounce": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/redux-localstorage-debounce/-/redux-localstorage-debounce-0.1.0.tgz", "integrity": "sha512-1SMGRkUhsH3SHp2x1yse1/4FKIlqSjxvvDP63J7ike8vGI1lWRatG8gSRyhDDcfjNagvnzhoY57P36HcvFXFqw==", + "license": "MIT", "dependencies": { "lodash.debounce": "^3.1.1" } @@ -7123,6 +7264,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-3.1.1.tgz", "integrity": "sha512-lcmJwMpdPAtChA4hfiwxTtgFeNAaow701wWUgVUqeD0XJF7vMXIN+bu/2FJSGxT0NUbZy9g9VFrlOFfPjl+0Ew==", + "license": "MIT", "dependencies": { "lodash._getnative": "^3.0.0" } @@ -7130,20 +7272,23 @@ "node_modules/redux-localstorage-filter": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/redux-localstorage-filter/-/redux-localstorage-filter-0.1.1.tgz", - "integrity": "sha512-qWx0stDxleQJEO0M4n7DNCWb7VJa+FzOSpYaMPq6aUIYlmlQf9rc197+6uvbNsW3Jsc4G/SYrGPd9s8KQpP5pg==" + "integrity": "sha512-qWx0stDxleQJEO0M4n7DNCWb7VJa+FzOSpYaMPq6aUIYlmlQf9rc197+6uvbNsW3Jsc4G/SYrGPd9s8KQpP5pg==", + "license": "MIT" }, "node_modules/redux-saga": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.3.tgz", - "integrity": "sha512-HDe0wTR5nhd8Xr5xjGzoyTbdAw6rjy1GDplFt3JKtKN8/MnkQSRqK/n6aQQhpw5NI4ekDVOaW+w4sdxPBaCoTQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.3.0.tgz", + "integrity": "sha512-J9RvCeAZXSTAibFY0kGw6Iy4EdyDNW7k6Q+liwX+bsck7QVsU78zz8vpBRweEfANxnnlG/xGGeOvf6r8UXzNJQ==", + "license": "MIT", "dependencies": { - "@redux-saga/core": "^1.2.3" + "@redux-saga/core": "^1.3.0" } }, "node_modules/refractor": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "license": "MIT", "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", @@ -7158,6 +7303,7 @@ "version": "1.27.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -7166,19 +7312,22 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/reftools/-/reftools-1.1.9.tgz", "integrity": "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==", + "license": "BSD-3-Clause", "funding": { "url": "https://github.com/Mermade/oas-kit?sponsor=1" } }, "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" }, "node_modules/remarkable": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-2.0.1.tgz", "integrity": "sha512-YJyMcOH5lrR+kZdmB0aJJ4+93bEojRZ1HGDn9Eagu6ibg7aVZhc3OWbbShRid+Q5eAfsEqWxpe+g5W5nYNfNiA==", + "license": "MIT", "dependencies": { "argparse": "^1.0.10", "autolinker": "^3.11.0" @@ -7193,12 +7342,14 @@ "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==" + "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", + "license": "ISC" }, "node_modules/repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "license": "MIT", "engines": { "node": ">=0.10" } @@ -7207,6 +7358,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7215,6 +7367,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7222,17 +7375,20 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "license": "MIT" }, "node_modules/reselect": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", - "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "license": "MIT" }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", "engines": { "node": ">=8" } @@ -7240,20 +7396,23 @@ "node_modules/resolve-pathname": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==" + "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", + "license": "MIT" }, "node_modules/ret": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -7277,6 +7436,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -7298,28 +7458,20 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", "dependencies": { "loose-envify": "^1.1.0" } @@ -7327,24 +7479,24 @@ "node_modules/seamless-immutable": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/seamless-immutable/-/seamless-immutable-7.1.4.tgz", - "integrity": "sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==" + "integrity": "sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==", + "license": "BSD-3-Clause" }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "optional": true, + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "peer": true, "bin": { "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -7368,6 +7520,7 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -7375,17 +7528,23 @@ "node_modules/send/node_modules/debug/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, "node_modules/serialize-error": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -7397,14 +7556,15 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -7414,6 +7574,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -7435,6 +7596,7 @@ "https://paypal.me/jonathanschlinkert", "https://jonschlinkert.dev/sponsor" ], + "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", "is-primitive": "^3.0.1" @@ -7446,12 +7608,14 @@ "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" }, "node_modules/sha.js": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "license": "(MIT AND BSD-3-Clause)", "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -7463,12 +7627,14 @@ "node_modules/shallowequal": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", + "license": "MIT" }, "node_modules/short-unique-id": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-5.2.0.tgz", - "integrity": "sha512-cMGfwNyfDZ/nzJ2k2M+ClthBIh//GlZl1JEf47Uoa9XR11bz8Pa2T2wQO4bVrRdH48LrIDWJahQziKo3MjhsWg==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/short-unique-id/-/short-unique-id-5.3.2.tgz", + "integrity": "sha512-KRT/hufMSxXKEDSQujfVE0Faa/kZ51ihUcZQAcmP04t00DvPj7Ox5anHke1sJYUtzSuiT/Y5uyzg/W7bBEGhCg==", + "license": "Apache-2.0", "bin": { "short-unique-id": "bin/short-unique-id", "suid": "bin/short-unique-id" @@ -7478,6 +7644,7 @@ "version": "13.2.3", "resolved": "https://registry.npmjs.org/should/-/should-13.2.3.tgz", "integrity": "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ==", + "license": "MIT", "dependencies": { "should-equal": "^2.0.0", "should-format": "^3.0.3", @@ -7490,6 +7657,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/should-equal/-/should-equal-2.0.0.tgz", "integrity": "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==", + "license": "MIT", "dependencies": { "should-type": "^1.4.0" } @@ -7498,6 +7666,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/should-format/-/should-format-3.0.3.tgz", "integrity": "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q==", + "license": "MIT", "dependencies": { "should-type": "^1.3.0", "should-type-adaptors": "^1.0.1" @@ -7506,12 +7675,14 @@ "node_modules/should-type": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/should-type/-/should-type-1.4.0.tgz", - "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==" + "integrity": "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ==", + "license": "MIT" }, "node_modules/should-type-adaptors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz", "integrity": "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA==", + "license": "MIT", "dependencies": { "should-type": "^1.3.0", "should-util": "^1.0.0" @@ -7520,17 +7691,36 @@ "node_modules/should-util": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/should-util/-/should-util-1.0.1.tgz", - "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==" + "integrity": "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g==", + "license": "MIT" }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" }, "engines": { "node": ">= 0.4" @@ -7539,55 +7729,48 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-concat": { + "node_modules/side-channel-map": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "optional": true + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "optional": true, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", "engines": { "node": ">=8" } @@ -7596,23 +7779,16 @@ "version": "1.4.7", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.4.7.tgz", "integrity": "sha512-tf+h5W1IrjNm/9rKKj0JU2MDMruiopx0jjVA5zCdBtcGjfp0+c5rHw/zADLC3IeKlGHtVbHtpfzvYA0OYT+HKg==", + "license": "MIT", "engines": { "node": ">=8.0.0" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", "peer": true, "engines": { "node": ">=0.10.0" @@ -7622,6 +7798,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -7630,23 +7807,14 @@ "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -7668,28 +7836,22 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "optional": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-env-interpolation": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string-env-interpolation/-/string-env-interpolation-1.0.1.tgz", - "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==" + "integrity": "sha512-78lwMoCcn0nNu8LszbP1UA7g55OeE4v7rCeWnM5B453rnNr4aq+5it3FEYtZrSEiMvHZOZ9Jlqb0OD0M2VInqg==", + "license": "MIT" }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7703,6 +7865,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7710,34 +7873,30 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "optional": true, - "engines": { - "node": ">=0.10.0" - } + "node_modules/strnum": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", + "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" }, "node_modules/style-mod": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "license": "MIT", "peer": true }, - "node_modules/style-value-types": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-5.0.0.tgz", - "integrity": "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==", - "dependencies": { - "hey-listen": "^1.0.8", - "tslib": "^2.1.0" - } - }, "node_modules/styled-components": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.11.tgz", - "integrity": "sha512-Ui0jXPzbp1phYij90h12ksljKGqF8ncGx+pjrNPsSPhbUUjWT2tD1FwGo2LF6USCnbrsIhNngDfodhxbegfEOA==", + "version": "6.1.18", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.18.tgz", + "integrity": "sha512-Mvf3gJFzZCkhjY2Y/Fx9z1m3dxbza0uI9H1CbNZm/jSHCojzJhQ0R7bByrlFJINnMzz/gPulpoFFGymNwrsMcw==", + "license": "MIT", "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.2.2", @@ -7745,7 +7904,7 @@ "@types/stylis": "4.2.5", "css-to-react-native": "3.2.0", "csstype": "3.1.3", - "postcss": "8.4.38", + "postcss": "8.4.49", "shallowequal": "1.1.0", "stylis": "4.3.2", "tslib": "2.6.2" @@ -7762,25 +7921,18 @@ "react-dom": ">= 16.8.0" } }, - "node_modules/styled-components/node_modules/@emotion/is-prop-valid": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz", - "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==", - "peer": true, - "dependencies": { - "@emotion/memoize": "^0.8.1" - } - }, - "node_modules/styled-components/node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==", + "node_modules/styled-components/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "license": "0BSD", "peer": true }, "node_modules/stylis": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz", "integrity": "sha512-bhtUjWd/z6ltJiQwg0dUfxEJ+W+jdqQd8TbWLWyeIJHlnsqmGLRFFd8e5mA0AZi/zx90smXRlN66YMTcaSFifg==", + "license": "MIT", "peer": true }, "node_modules/subscriptions-transport-ws": { @@ -7788,6 +7940,7 @@ "resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.19.tgz", "integrity": "sha512-dxdemxFFB0ppCLg10FTtRqH/31FNRL1y1BQv8209MK5I4CwALb7iihQg+7p65lFcIl8MHatINWBLOqpgU4Kyyw==", "deprecated": "The `subscriptions-transport-ws` package is no longer maintained. We recommend you use `graphql-ws` instead. For help migrating Apollo software to `graphql-ws`, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws For general help using `graphql-ws`, see https://github.com/enisdenjo/graphql-ws/blob/master/README.md", + "license": "MIT", "dependencies": { "backo2": "^1.0.2", "eventemitter3": "^3.1.0", @@ -7802,80 +7955,58 @@ "node_modules/subscriptions-transport-ws/node_modules/eventemitter3": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==" - }, - "node_modules/subscriptions-transport-ws/node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } + "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", + "license": "MIT" }, "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/swagger-client": { - "version": "3.28.1", - "resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.28.1.tgz", - "integrity": "sha512-tt3/54GTImgOLrjzl83FZ+koJ7Kq6uuyBNS7mTpZeUQsBi2a/4IvqPcfY2qKhf7CFrbv6lzPm+MmSudrxU8J5g==", + "version": "3.35.5", + "resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-3.35.5.tgz", + "integrity": "sha512-ayCrpDAgm5jIdq1kmcVWJRfp27cqU9tSRiAfKg3BKeplOmvu3+lKTPPtz4x1uI8v5l5/92Aopvq0EzRkXEr7Rw==", + "license": "Apache-2.0", "dependencies": { "@babel/runtime-corejs3": "^7.22.15", - "@swagger-api/apidom-core": ">=1.0.0-alpha.3 <1.0.0-beta.0", - "@swagger-api/apidom-error": ">=1.0.0-alpha.1 <1.0.0-beta.0", - "@swagger-api/apidom-json-pointer": ">=1.0.0-alpha.3 <1.0.0-beta.0", - "@swagger-api/apidom-ns-openapi-3-1": ">=1.0.0-alpha.3 <1.0.0-beta.0", - "@swagger-api/apidom-reference": ">=1.0.0-alpha.3 <1.0.0-beta.0", - "cookie": "~0.6.0", + "@scarf/scarf": "=1.4.0", + "@swagger-api/apidom-core": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-error": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-json-pointer": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-ns-openapi-3-1": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swagger-api/apidom-reference": ">=1.0.0-beta.41 <1.0.0-rc.0", + "@swaggerexpert/cookie": "^2.0.2", "deepmerge": "~4.3.0", "fast-json-patch": "^3.0.0-1", - "is-plain-object": "^5.0.0", "js-yaml": "^4.1.0", + "neotraverse": "=0.6.18", "node-abort-controller": "^3.1.1", "node-fetch-commonjs": "^3.3.2", - "openapi-path-templating": "^1.5.1", - "qs": "^6.10.2", - "ramda-adjunct": "^5.0.0", - "traverse": "=0.6.8" + "openapi-path-templating": "^2.2.1", + "openapi-server-url-templating": "^1.3.0", + "ramda": "^0.30.1", + "ramda-adjunct": "^5.1.0" } }, "node_modules/swagger-client/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, - "node_modules/swagger-client/node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "engines": { - "node": ">=0.10.0" - } + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/swagger-client/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -7884,17 +8015,18 @@ } }, "node_modules/swagger-ui": { - "version": "5.17.14", - "resolved": "https://registry.npmjs.org/swagger-ui/-/swagger-ui-5.17.14.tgz", - "integrity": "sha512-z9pOymwowrgkoMKNsRSN6QjOyYrMAnc4iohEebnS/ELT5jjdB13Lu4f3Bl4+o2Mw5B6QOyo4vibr/A/Vb7UbMQ==", + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/swagger-ui/-/swagger-ui-5.24.0.tgz", + "integrity": "sha512-jX/9fqlXWGV44GP8CEwJxulDhVSzHLO8VwWne7Do223X4xdGjiZrRtxYUR0X5RhmTiJuJq64ZKDch57PtqNTpA==", + "license": "Apache-2.0", "dependencies": { - "@babel/runtime-corejs3": "^7.24.5", - "@braintree/sanitize-url": "=7.0.2", + "@babel/runtime-corejs3": "^7.27.1", + "@scarf/scarf": "=1.4.0", "base64-js": "^1.5.1", "classnames": "^2.5.1", "css.escape": "1.5.1", "deep-extend": "0.6.0", - "dompurify": "=3.1.4", + "dompurify": "=3.2.4", "ieee754": "^1.2.1", "immutable": "^3.x.x", "js-file-download": "^0.4.12", @@ -7910,15 +8042,15 @@ "react-immutable-proptypes": "2.2.0", "react-immutable-pure-component": "^2.2.0", "react-inspector": "^6.0.1", - "react-redux": "^9.1.2", - "react-syntax-highlighter": "^15.5.0", + "react-redux": "^9.2.0", + "react-syntax-highlighter": "^15.6.1", "redux": "^5.0.1", "redux-immutable": "^4.0.0", "remarkable": "^2.0.1", - "reselect": "^5.1.0", + "reselect": "^5.1.1", "serialize-error": "^8.1.0", "sha.js": "^2.4.11", - "swagger-client": "^3.28.1", + "swagger-client": "^3.35.5", "url-parse": "^1.5.10", "xml": "=1.0.1", "xml-but-prettier": "^1.0.1", @@ -7928,12 +8060,23 @@ "node_modules/swagger-ui/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/swagger-ui/node_modules/dompurify": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz", + "integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/swagger-ui/node_modules/immutable": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7942,6 +8085,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -7950,16 +8094,17 @@ } }, "node_modules/swagger-ui/node_modules/react-redux": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", - "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", "dependencies": { - "@types/use-sync-external-store": "^0.0.3", - "use-sync-external-store": "^1.0.0" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" }, "peerDependencies": { - "@types/react": "^18.2.25", - "react": "^18.0", + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", "redux": "^5.0.0" }, "peerDependenciesMeta": { @@ -7974,17 +8119,20 @@ "node_modules/swagger-ui/node_modules/redux": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" }, "node_modules/swagger-ui/node_modules/reselect": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.0.tgz", - "integrity": "sha512-aw7jcGLDpSgNDyWBQLv2cedml85qd95/iszJjN988zX1t7AVRJi19d9kto5+W7oCfQ94gyo40dVbT6g2k4/kXg==" + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" }, "node_modules/swagger2openapi": { "version": "7.0.8", "resolved": "https://registry.npmjs.org/swagger2openapi/-/swagger2openapi-7.0.8.tgz", "integrity": "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g==", + "license": "BSD-3-Clause", "dependencies": { "call-me-maybe": "^1.0.1", "node-fetch": "^2.6.1", @@ -8011,6 +8159,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" }, @@ -8026,101 +8175,53 @@ } } }, - "node_modules/swagger2openapi/node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, - "node_modules/swagger2openapi/node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, - "node_modules/swagger2openapi/node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" - }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "optional": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "optional": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" }, "node_modules/tiny-invariant": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" }, "node_modules/to-camel-case": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz", "integrity": "sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==", + "license": "MIT", "dependencies": { "to-space-case": "^1.0.0" } }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "engines": { - "node": ">=4" - } - }, "node_modules/to-no-case": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz", - "integrity": "sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==" + "integrity": "sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==", + "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -8132,6 +8233,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz", "integrity": "sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==", + "license": "MIT", "dependencies": { "to-no-case": "^1.0.0" } @@ -8139,92 +8241,67 @@ "node_modules/toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", "engines": { "node": ">=0.6" } }, - "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", - "dependencies": { - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/traverse": { - "version": "0.6.8", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", - "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" }, "node_modules/tree-sitter": { - "version": "0.20.4", - "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.20.4.tgz", - "integrity": "sha512-rjfR5dc4knG3jnJNN/giJ9WOoN1zL/kZyrS0ILh+eqq8RNcIbiXA63JsMEgluug0aNvfQvK4BfCErN1vIzvKog==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/tree-sitter/-/tree-sitter-0.21.1.tgz", + "integrity": "sha512-7dxoA6kYvtgWw80265MyqJlkRl4yawIjO7S5MigytjELkX43fV2WsAXzsNfO7sBpPPCF5Gp0+XzHk0DwLCq3xQ==", "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { - "nan": "^2.17.0", - "prebuild-install": "^7.1.1" + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0" } }, "node_modules/tree-sitter-json": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.20.2.tgz", - "integrity": "sha512-eUxrowp4F1QEGk/i7Sa+Xl8Crlfp7J0AXxX1QdJEQKQYMWhgMbCIgyQvpO3Q0P9oyTrNQxRLlRipDS44a8EtRw==", - "hasInstallScript": true, - "optional": true, - "dependencies": { - "nan": "^2.18.0" - } - }, - "node_modules/tree-sitter-yaml": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/tree-sitter-yaml/-/tree-sitter-yaml-0.5.0.tgz", - "integrity": "sha512-POJ4ZNXXSWIG/W4Rjuyg36MkUD4d769YRUGKRqN+sVaj/VCo6Dh6Pkssn1Rtewd5kybx+jT1BWMyWN0CijXnMA==", + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/tree-sitter-json/-/tree-sitter-json-0.24.8.tgz", + "integrity": "sha512-Tc9ZZYwHyWZ3Tt1VEw7Pa2scu1YO7/d2BCBbKTx5hXwig3UfdQjsOPkPyLpDJOn/m1UBEWYAtSdGAwCSyagBqQ==", "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { - "nan": "^2.14.0" + "node-addon-api": "^8.2.2", + "node-gyp-build": "^4.8.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree-sitter": { + "optional": true + } } }, "node_modules/tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==" + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "license": "MIT" }, "node_modules/ts-invariant": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "license": "MIT", "dependencies": { "tslib": "^1.9.3" } @@ -8232,47 +8309,32 @@ "node_modules/ts-invariant/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/ts-mixer": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", - "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==" + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" }, "node_modules/ts-toolbelt": { "version": "9.6.0", "resolved": "https://registry.npmjs.org/ts-toolbelt/-/ts-toolbelt-9.6.0.tgz", - "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==" + "integrity": "sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==", + "license": "Apache-2.0" }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "optional": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "engines": { - "node": ">=4" - } + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -8284,6 +8346,7 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -8293,9 +8356,10 @@ } }, "node_modules/types-ramda": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.30.0.tgz", - "integrity": "sha512-oVPw/KHB5M0Du0txTEKKM8xZOG9cZBRdCVXvwHYuNJUVkAiJ9oWyqkA+9Bj2gjMsHgkkhsYevobQBWs8I2/Xvw==", + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/types-ramda/-/types-ramda-0.30.1.tgz", + "integrity": "sha512-1HTsf5/QVRmLzcGfldPFvkVsAdi1db1BBKzi7iW3KBUlOICg/nKnFS+jGqDJS3YD8VsWbAh7JiHeBvbsw8RPxA==", + "license": "MIT", "dependencies": { "ts-toolbelt": "^9.6.0" } @@ -8304,6 +8368,7 @@ "version": "0.0.2", "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "license": "MIT", "dependencies": { "typescript-logic": "^0.0.0" } @@ -8311,12 +8376,14 @@ "node_modules/typescript-logic": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", - "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==", + "license": "MIT" }, "node_modules/typescript-tuple": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", + "license": "MIT", "dependencies": { "typescript-compare": "^0.0.2" } @@ -8324,20 +8391,20 @@ "node_modules/uc.micro": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==" + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "engines": { - "node": ">= 4.0.0" - } + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" }, "node_modules/unixify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz", "integrity": "sha512-6bc58dPYhCMHHuwxldQxO3RRNZ4eCogZ/st++0+fcC1nr0jiGUtAdBJ2qzmLQWSxbtz42pWt4QQMiZ9HvZf5cg==", + "license": "MIT", "dependencies": { "normalize-path": "^2.1.1" }, @@ -8349,6 +8416,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -8356,12 +8424,13 @@ "node_modules/unraw": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unraw/-/unraw-3.0.0.tgz", - "integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==" + "integrity": "sha512-08/DA66UF65OlpUDIQtbJyrqTR0jTAlJ+jsnkQ4jxR7+K5g5YG1APZKQSMCE1vqqmD+2pv6+IdEjmopFatacvg==", + "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", - "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "funding": [ { "type": "opencollective", @@ -8376,10 +8445,11 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "peer": true, "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -8388,18 +8458,17 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dependencies": { - "punycode": "^2.1.0" - } + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "license": "MIT" }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "license": "MIT", "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -8408,17 +8477,20 @@ "node_modules/url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", - "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", + "license": "BSD" }, "node_modules/urlpattern-polyfill": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", - "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==" + "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==", + "license": "MIT" }, "node_modules/use-callback-ref": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.0.tgz", - "integrity": "sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", "dependencies": { "tslib": "^2.0.0" }, @@ -8426,8 +8498,8 @@ "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -8436,9 +8508,10 @@ } }, "node_modules/use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" @@ -8447,8 +8520,8 @@ "node": ">=10" }, "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { "@types/react": { @@ -8457,23 +8530,19 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "optional": true - }, "node_modules/utility-types": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-1.1.0.tgz", "integrity": "sha512-6PGyowB/ZDDAygpdZzdLu/9mn2EMf08/V1OOqTTc5EhADgd+/BQhinslzhD9xTVw3EWYa1jI3aBMZy5neBbSfw==", + "license": "MIT", "engines": { "node": ">= 4" } @@ -8482,6 +8551,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", "engines": { "node": ">= 0.4.0" } @@ -8489,12 +8559,14 @@ "node_modules/value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==" + "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", + "license": "MIT" }, "node_modules/value-or-promise": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==", + "license": "MIT", "engines": { "node": ">=12" } @@ -8503,36 +8575,29 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/vscode-languageserver-types": { - "version": "3.17.3", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz", - "integrity": "sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==" + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" }, "node_modules/w3c-keyname": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT", "peer": true }, - "node_modules/w3c-xmlserializer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", - "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", - "dependencies": { - "xml-name-validator": "^4.0.0" - }, - "engines": { - "node": ">=14" - } - }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "license": "MIT", "dependencies": { "loose-envify": "^1.0.0" } @@ -8541,40 +8606,42 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/web-tree-sitter": { - "version": "0.20.3", - "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.20.3.tgz", - "integrity": "sha512-zKGJW9r23y3BcJusbgvnOH2OYAW40MXAOi9bi3Gcc7T4Gms9WWgXF8m6adsJWpGJEhgOzCrfiz1IzKowJWrtYw==", + "version": "0.24.5", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.24.5.tgz", + "integrity": "sha512-+J/2VSHN8J47gQUAvF8KDadrfz6uFYVjxoxbKWDoXVsH2u7yLdarCnIURnrMA6uSRkgX3SdmqM5BOoQjPdSh5w==", + "license": "MIT", "optional": true }, "node_modules/webcrypto-core": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.7.7.tgz", - "integrity": "sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.1.tgz", + "integrity": "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==", + "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.3.6", + "@peculiar/asn1-schema": "^2.3.13", "@peculiar/json-schema": "^1.1.12", - "asn1js": "^3.0.1", - "pvtsutils": "^1.3.2", - "tslib": "^2.4.0" + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.5", + "tslib": "^2.7.0" } }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "engines": { - "node": ">=12" - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" }, "node_modules/webpack-bundle-analyzer": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.0.tgz", "integrity": "sha512-Ob8amZfCm3rMB1ScjQVlbYYUEJyEjdEtQ92jqiFUYt5VkEeO2v5UMbv49P/gnmCZm3A6yaFQzCBvpZqN4MUsdA==", + "license": "MIT", "dependencies": { "acorn": "^7.1.1", "acorn-walk": "^7.1.1", @@ -8597,148 +8664,36 @@ "node": ">= 6.14.4" } }, - "node_modules/webpack-bundle-analyzer/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/webpack-bundle-analyzer/node_modules/ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "license": "MIT", "dependencies": { "async-limiter": "~1.0.0" } }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/whatwg-fetch": { - "version": "3.6.19", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz", - "integrity": "sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw==" - }, - "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", - "engines": { - "node": ">=12" - } + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" }, "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", "dependencies": { - "tr46": "^3.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8751,22 +8706,50 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "optional": true + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/ws": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", - "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=8.3.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" + "utf-8-validate": "^5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -8780,33 +8763,23 @@ "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" }, "node_modules/xml-but-prettier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml-but-prettier/-/xml-but-prettier-1.0.1.tgz", "integrity": "sha512-C2CJaadHrZTqESlH03WOyw0oZTtoy2uEg6dSDF6YRg+9GnYNub53RRemLpnvtbHDFelxMx4LajiFsYeR6XJHgQ==", + "license": "MIT", "dependencies": { "repeat-string": "^1.5.2" } }, - "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", - "engines": { - "node": ">=12" - } - }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", "engines": { "node": ">=0.4" } @@ -8815,20 +8788,22 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "peer": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", "engines": { "node": ">= 6" } @@ -8836,12 +8811,14 @@ "node_modules/yaml-ast-parser": { "version": "0.0.43", "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", - "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==" + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "license": "Apache-2.0" }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -8859,6 +8836,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", "engines": { "node": ">=12" } @@ -8867,6 +8845,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", "engines": { "node": ">=10" }, @@ -8877,12 +8856,14 @@ "node_modules/zen-observable": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.7.1.tgz", - "integrity": "sha512-OI6VMSe0yeqaouIXtedC+F55Sr6r9ppS7+wTbSexkYdHbdt4ctTuPNXP/rwm7GTVI63YBc+EBT0b0tl7YnJLRg==" + "integrity": "sha512-OI6VMSe0yeqaouIXtedC+F55Sr6r9ppS7+wTbSexkYdHbdt4ctTuPNXP/rwm7GTVI63YBc+EBT0b0tl7YnJLRg==", + "license": "MIT" }, "node_modules/zen-observable-ts": { "version": "0.8.21", "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.21.tgz", "integrity": "sha512-Yj3yXweRc8LdRMrCC8nIc4kkjWecPAUVh0TI0OUrWXx6aX790vLcDlWca6I4vsyCGH3LpWxq0dJRcMOFoVqmeg==", + "license": "MIT", "dependencies": { "tslib": "^1.9.3", "zen-observable": "^0.8.0" @@ -8891,17 +8872,49 @@ "node_modules/zen-observable-ts/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, "node_modules/zen-observable-ts/node_modules/zen-observable": { "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", - "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==", + "license": "MIT" }, "node_modules/zenscroll": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/zenscroll/-/zenscroll-4.0.2.tgz", - "integrity": "sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==" + "integrity": "sha512-jEA1znR7b4C/NnaycInCU6h/d15ZzCd1jmsruqOKnZP6WXQSMH3W2GL+OXbkruslU4h+Tzuos0HdswzRUk/Vgg==", + "license": "Unlicense" + }, + "node_modules/zustand": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.5.tgz", + "integrity": "sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 03e6f9f4d42..ef1bbd402a6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,22 +1,24 @@ parameters: - level: 5 + level: 6 paths: - src - tests - tests/Fixtures/app/console inferPrivatePropertyTypeFromConstructor: true + treatPhpDocTypesAsCertain: false symfony: containerXmlPath: tests/Fixtures/app/var/cache/test/AppKernelTestDebugContainer.xml constantHassers: false doctrine: objectManagerLoader: tests/Fixtures/app/object-manager.php bootstrapFiles: - - vendor/bin/.phpunit/phpunit/vendor/autoload.php # We're aliasing classes for phpunit in this file, it needs to be added here see phpstan/#2194 - src/Symfony/Bundle/Test/Constraint/ArraySubset.php - tests/Fixtures/app/AppKernel.php + excludePaths: - - src/Symfony/Bundle/Command/OpenApiCommand.php + # uses larastan + - src/Laravel # Symfony config - tests/Fixtures/app/config/config_swagger.php # Symfony cache @@ -32,10 +34,6 @@ parameters: - src/Doctrine/*/vendor/* - src/*/vendor/* # Symfony 6 support - - src/OpenApi/Serializer/CacheableSupportsMethodInterface.php - - src/Serializer/CacheableSupportsMethodInterface.php - - tests/Hal/Serializer/ItemNormalizerTest.php - - tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php - src/Symfony/Bundle/ArgumentResolver/CompatibleValueResolverInterface.php earlyTerminatingMethodCalls: PHPUnit\Framework\Constraint\Constraint: @@ -56,24 +54,24 @@ parameters: - tests/Fixtures/TestBundle/Document/ - tests/Fixtures/TestBundle/Entity/ - src/OpenApi/Factory/OpenApiFactory.php + - + message: '#is never assigned .* so it can be removed from the property type.#' + paths: + - src/Doctrine/Common/Tests/Fixtures/ + - src/Doctrine/Orm/Tests/Fixtures/ + - src/Doctrine/Odm/Tests/Fixtures/ + - src/Elasticsearch/Tests/Fixtures/ + - src/GraphQl/Tests/Fixtures/ + - src/JsonSchema/Tests/Fixtures/ + - src/Metadata/Tests/Fixtures/ + - src/Serializer/Tests/Fixtures/ + - tests/Fixtures/ - message: '#is never written, only read.#' paths: - tests/Fixtures/TestBundle/Document/ - src/Metadata/Tests/Fixtures/ApiResource/ - - - message: '#Parameter \#1 \$constraint of method#' - paths: - - tests/Symfony/Validator/Metadata/Property/Restriction/ - - - message: '#expects ApiPlatform\\Metadata\\GraphQl\\Operation\|null, ApiPlatform\\Metadata\\Operation given#' - paths: - - src/GraphQl/Tests/Resolver/Factory/ - '#Access to an undefined property Prophecy\\Prophecy\\ObjectProphecy<(\\?[a-zA-Z0-9_]+)+>::\$[a-zA-Z0-9_]+#' - # https://github.com/willdurand/Negotiation/issues/89#issuecomment-513283286 - - - message: '#Call to an undefined method Negotiation\\AcceptHeader::getType\(\)\.#' - path: src/Symfony/EventListener/AddFormatListener.php # https://github.com/phpstan/phpstan-symfony/issues/76 - message: '#Service "test" is not registered in the container\.#' @@ -84,20 +82,99 @@ parameters: - '#Method Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface::normalize\(\) invoked with (2|3|4) parameters, 1 required\.#' # Expected, due to backward compatibility - - - message: "#Call to function method_exists\\(\\) with ApiPlatform\\\\JsonApi\\\\Serializer\\\\ItemNormalizer and 'setCircularReferenc…' will always evaluate to false\\.#" - path: tests/JsonApi/Serializer/ItemNormalizerTest.php - '#Method GraphQL\\Type\\Definition\\WrappingType::getWrappedType\(\) invoked with 1 parameter, 0 required\.#' - '#Access to an undefined property GraphQL\\Type\\Definition\\NamedType&GraphQL\\Type\\Definition\\Type::\$name\.#' + - "#Call to function method_exists\\(\\) with GraphQL\\\\Type\\\\Definition\\\\Type&GraphQL\\\\Type\\\\Definition\\\\WrappingType and 'getInnermostType' will always evaluate to true\\.#" + - "#Call to function method_exists\\(\\) with Symfony\\\\Component\\\\Console\\\\Application and 'addCommand' will always evaluate to false\\.#" + - "#Call to function method_exists\\(\\) with 'Symfony\\\\\\\\Component\\\\\\\\PropertyInfo\\\\\\\\PropertyInfoExtractor' and 'getType' will always evaluate to true\\.#" + - "#Call to function method_exists\\(\\) with 'Symfony\\\\\\\\Component\\\\\\\\HttpFoundation\\\\\\\\Request' and 'getContentTypeFormat' will always evaluate to true\\.#" + - '#Call to an undefined method Symfony\\Component\\HttpFoundation\\Request::getContentType\(\)\.#' + - "#Call to function method_exists\\(\\) with 'Symfony\\\\\\\\Component\\\\\\\\Serializer\\\\\\\\Serializer' and 'getSupportedTypes' will always evaluate to true\\.#" + - "#Call to function method_exists\\(\\) with Symfony\\\\Component\\\\Serializer\\\\Normalizer\\\\NormalizerInterface and 'getSupportedTypes' will always evaluate to true\\.#" + - "#Call to function method_exists\\(\\) with Doctrine\\\\ODM\\\\MongoDB\\\\Configuration and 'setMetadataCache' will always evaluate to true\\.#" + - "#Call to function method_exists\\(\\) with Doctrine\\\\ODM\\\\MongoDB\\\\Mapping\\\\ClassMetadata\\|Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata and 'isChangeTrackingDef…' will always evaluate to true\\.#" + - + message: '#Instanceof between Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface and Symfony\\Component\\Serializer\\NameConverter\\MetadataAwareNameConverter will always evaluate to false\.#' + paths: + - src/GraphQl/Serializer/SerializerContextBuilder.php + - src/GraphQl/Type/FieldsBuilder.php + - + message: '#Instanceof between Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface\|null and Symfony\\Component\\Serializer\\NameConverter\\MetadataAwareNameConverter will always evaluate to false\.#' + paths: + - src/Serializer/AbstractConstraintViolationListNormalizer.php + - src/Symfony/Validator/Serializer/ValidationExceptionNormalizer.php + # See https://github.com/phpstan/phpstan-symfony/issues/27 - message: '#^Service "[^"]+" is private.$#' path: src + + + # Allow extra assertions in tests: https://github.com/phpstan/phpstan-strict-rules/issues/130 + - '#^Call to (static )?method PHPUnit\\Framework\\Assert::.* will always evaluate to true\.$#' + + # Unsealed array shapes not supported + - + message: '#^Parameter &\$context by\-ref type of method ApiPlatform\\Doctrine\\Odm\\Extension\\ParameterExtension\:\:applyFilter\(\) expects array\, array(.*) given\.$#' + identifier: parameterByRef.type + count: 5 + path: src/Doctrine/Odm/Extension/ParameterExtension.php + + # Level 6 + - + identifier: missingType.iterableValue + - + identifier: missingType.generics - - message: '#^Class .+ not found.$#' - path: src/Elasticsearch/Tests - # Backward compatibility - - '#Call to method hasCacheableSupportsMethod\(\) on an unknown class Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface\.#' - - '#Class Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface not found\.#' - - '#Access to undefined constant Symfony\\Component\\HttpKernel\\HttpKernelInterface::MASTER_REQUEST\.#' - - '#Attribute class PHPUnit\\Framework\\Attributes\\DataProvider does not exist.#' + identifier: missingType.property + paths: + - src/Doctrine/Common/Tests + - src/Doctrine/Odm/Tests + - src/Doctrine/Orm/Tests + - src/Elasticsearch/Tests + - src/Hydra/Tests + - src/JsonApi/Tests + - src/JsonSchema/Tests + - src/Metadata/Tests + - src/Serializer/Tests + - src/Symfony/Tests + - tests + - + identifier: missingType.parameter + paths: + - src/Doctrine/Common/Tests + - src/Doctrine/Odm/Tests + - src/Doctrine/Orm/Tests + - src/Elasticsearch/Tests + - src/GraphQl/Tests + - src/Hydra/Tests + - src/JsonApi/Tests + - src/JsonSchema/Tests + - src/Metadata/Tests + - src/OpenApi/Tests + - src/Serializer/Tests + - src/Symfony/Bundle/Test + - src/Symfony/Tests + - tests + - + identifier: missingType.return + paths: + - src/Doctrine/Common/Tests + - src/Doctrine/Odm/Tests + - src/Doctrine/Orm/Tests + - src/Elasticsearch/Tests + - src/GraphQl/Tests + - src/Hydra/Tests + - src/JsonApi/Tests + - src/JsonSchema/Tests + - src/Metadata/Tests + - src/OpenApi/Tests + - src/Serializer/Tests + - src/Symfony/Tests + - tests + - + identifier: argument.templateType + paths: + - src/Symfony/Bundle/Test + - tests + - src # TODO diff --git a/phpunit.baseline.xml b/phpunit.baseline.xml new file mode 100644 index 00000000000..49cbb1c2b4d --- /dev/null +++ b/phpunit.baseline.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 230919fb769..d8527fddfa3 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,50 +1,47 @@ + + + + + + + + + + + - - - - - - - - - - - - - - - + + + tests + + - - - tests - - + + + mongodb + mercure + + - - - src - - - src/Symfony/Maker/Resources/skeleton - features - tests - vendor - src/**/Tests/ - src/Doctrine/**/Tests/ - .php-cs-fixer.dist.php - - - - - - - - - - mongodb - mercure - - + + + trigger_deprecation + Doctrine\Deprecations\Deprecation::trigger + Doctrine\Deprecations\Deprecation::delegateTriggerToBackend + + + . + + + tests + features + vendor + .php-cs-fixer.dist.php + + diff --git a/phpunit10.xml.dist b/phpunit10.xml.dist deleted file mode 100644 index 9c8c1d2588a..00000000000 --- a/phpunit10.xml.dist +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - - - tests - - - - - - mongodb - mercure - - - - - - . - - - features - tests - vendor - .php-cs-fixer.dist.php - - - diff --git a/pmu.baseline b/pmu.baseline new file mode 100644 index 00000000000..c5974acbbc6 --- /dev/null +++ b/pmu.baseline @@ -0,0 +1 @@ +Class "ApiPlatform\Serializer\SerializerContextBuilder" uses "ApiPlatform\Doctrine\Orm\State\Options" but it is not declared as dependency. diff --git a/src/Action/EntrypointAction.php b/src/Action/EntrypointAction.php deleted file mode 100644 index 94cf2a5e1a0..00000000000 --- a/src/Action/EntrypointAction.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Action; - -use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; -use ApiPlatform\State\ProcessorInterface; -use ApiPlatform\State\ProviderInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -/** - * Generates the API entrypoint. - * - * @deprecated use ApiPlatform\Documentation\Action\EntrypointAction - * - * @author Kévin Dunglas - */ -final class EntrypointAction -{ - public function __construct( - private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, - private readonly ?ProviderInterface $provider = null, - private readonly ?ProcessorInterface $processor = null, - private readonly array $documentationFormats = [], - ) { - } - - /** - * @return Entrypoint|Response - */ - public function __invoke(?Request $request = null) - { - if ($this->provider && $this->processor) { - $context = ['request' => $request]; - $operation = new Get(outputFormats: $this->documentationFormats, read: true, serialize: true, class: Entrypoint::class, provider: fn () => new Entrypoint($this->resourceNameCollectionFactory->create())); - $body = $this->provider->provide($operation, [], $context); - // see https://github.com/api-platform/core/issues/5845#issuecomment-1732400657 - if ($request && ($apiOperation = $request->attributes->get('_api_operation'))) { - $operation = $apiOperation; - } - - return $this->processor->process($body, $operation, [], $context); - } - - return new Entrypoint($this->resourceNameCollectionFactory->create()); - } -} diff --git a/src/Action/ExceptionAction.php b/src/Action/ExceptionAction.php deleted file mode 100644 index 78e08c7b6cb..00000000000 --- a/src/Action/ExceptionAction.php +++ /dev/null @@ -1,117 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Action; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface; -use ApiPlatform\Util\ErrorFormatGuesser; -use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface as ApiPlatformConstraintViolationListAwareExceptionInterface; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Serializer\SerializerInterface; -use Symfony\Component\Validator\ConstraintViolationListInterface; - -/** - * Renders a normalized exception for a given see [FlattenException](https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/ErrorHandler/Exception/FlattenException.php). - * - * @author Baptiste Meyer - * @author Kévin Dunglas - * - * @deprecated since API Platform 3 and Error resource is used {@see ApiPlatform\Symfony\EventListener\ErrorListener} - */ -final class ExceptionAction -{ - use OperationRequestInitiatorTrait; - - /** - * @param array $errorFormats A list of enabled error formats - * @param array $exceptionToStatus A list of exceptions mapped to their HTTP status code - */ - public function __construct(private readonly SerializerInterface $serializer, private readonly array $errorFormats, private readonly array $exceptionToStatus = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - /** - * Converts an exception to a JSON response. - */ - public function __invoke(FlattenException $exception, Request $request): Response - { - $operation = $this->initializeOperation($request); - $exceptionClass = $exception->getClass(); - $statusCode = $exception->getStatusCode(); - - $exceptionToStatus = array_merge( - $this->exceptionToStatus, - $operation ? $operation->getExceptionToStatus() ?? [] : $this->getOperationExceptionToStatus($request) - ); - - foreach ($exceptionToStatus as $class => $status) { - if (is_a($exceptionClass, $class, true)) { - $statusCode = $status; - - break; - } - } - - $headers = $exception->getHeaders(); - $format = ErrorFormatGuesser::guessErrorFormat($request, $this->errorFormats); - $headers['Content-Type'] = \sprintf('%s; charset=utf-8', $format['value'][0]); - $headers['X-Content-Type-Options'] = 'nosniff'; - $headers['X-Frame-Options'] = 'deny'; - - $context = ['statusCode' => $statusCode, 'rfc_7807_compliant_errors' => $operation?->getExtraProperties()['rfc_7807_compliant_errors'] ?? false]; - $error = $request->attributes->get('exception') ?? $exception; - if ($error instanceof ConstraintViolationListAwareExceptionInterface || $error instanceof ApiPlatformConstraintViolationListAwareExceptionInterface) { - $error = $error->getConstraintViolationList(); - } elseif (method_exists($error, 'getViolations') && $error->getViolations() instanceof ConstraintViolationListInterface) { - $error = $error->getViolations(); - } else { - $error = $exception; - } - - $serializerFormat = $format['key']; - if ('json' === $serializerFormat && 'application/problem+json' === $format['value'][0]) { - $serializerFormat = 'jsonproblem'; - } - - return new Response($this->serializer->serialize($error, $serializerFormat, $context), $statusCode, $headers); - } - - private function getOperationExceptionToStatus(Request $request): array - { - $attributes = RequestAttributesExtractor::extractAttributes($request); - - if ([] === $attributes) { - return []; - } - - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($attributes['resource_class']); - /** @var HttpOperation $operation */ - $operation = $resourceMetadataCollection->getOperation($attributes['operation_name'] ?? null); - $exceptionToStatus = [$operation->getExceptionToStatus() ?: []]; - - foreach ($resourceMetadataCollection as $resourceMetadata) { - /* @var ApiResource $resourceMetadata */ - $exceptionToStatus[] = $resourceMetadata->getExceptionToStatus() ?: []; - } - - return array_merge(...$exceptionToStatus); - } -} diff --git a/src/Api/CompositeIdentifierParser.php b/src/Api/CompositeIdentifierParser.php deleted file mode 100644 index 97a1fabe2d9..00000000000 --- a/src/Api/CompositeIdentifierParser.php +++ /dev/null @@ -1,65 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -/** - * Normalizes a composite identifier. - * - * @author Antoine Bluchet - * - * @deprecated - */ -final class CompositeIdentifierParser -{ - public const COMPOSITE_IDENTIFIER_REGEXP = '/(\w+)=(?<=\w=)(.*?)(?=;\w+=)|(\w+)=([^;]*);?$/'; - - private function __construct() - { - } - - /* - * Normalize takes a string and gives back an array of identifiers. - * - * For example: foo=0;bar=2 returns ['foo' => 0, 'bar' => 2]. - */ - public static function parse(string $identifier): array - { - $matches = []; - $identifiers = []; - $num = preg_match_all(self::COMPOSITE_IDENTIFIER_REGEXP, $identifier, $matches, \PREG_SET_ORDER); - - foreach ($matches as $i => $match) { - if ($i === $num - 1) { - $identifiers[$match[3]] = $match[4]; - continue; - } - $identifiers[$match[1]] = $match[2]; - } - - return $identifiers; - } - - /** - * Renders composite identifiers to string using: key=value;key2=value2. - */ - public static function stringify(array $identifiers): string - { - $composite = []; - foreach ($identifiers as $name => $value) { - $composite[] = \sprintf('%s=%s', $name, $value); - } - - return implode(';', $composite); - } -} diff --git a/src/Api/Entrypoint.php b/src/Api/Entrypoint.php deleted file mode 100644 index ec8f0b8ec39..00000000000 --- a/src/Api/Entrypoint.php +++ /dev/null @@ -1,35 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Resource\ResourceNameCollection; - -/** - * The first path you will see in the API. - * - * @deprecated use ApiPlatform\Documentation\Entrypoint - * - * @author Amrouche Hamza - */ -final class Entrypoint -{ - public function __construct(private readonly ResourceNameCollection $resourceNameCollection) - { - } - - public function getResourceNameCollection(): ResourceNameCollection - { - return $this->resourceNameCollection; - } -} diff --git a/src/Api/FilterInterface.php b/src/Api/FilterInterface.php deleted file mode 100644 index b0d23e57d89..00000000000 --- a/src/Api/FilterInterface.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -if (interface_exists(\ApiPlatform\Metadata\FilterInterface::class)) { - trigger_deprecation('api-platform', '3.3', \sprintf('%s is deprecated in favor of %s. This class will be removed in 4.0.', FilterInterface::class, \ApiPlatform\Metadata\FilterInterface::class)); - class_alias( - \ApiPlatform\Metadata\FilterInterface::class, - __NAMESPACE__.'\FilterInterface' - ); - - if (false) { // @phpstan-ignore-line - interface FilterInterface extends \ApiPlatform\Metadata\FilterInterface - { - } - } -} else { - /** - * Filters applicable on a resource. - * - * @author Kévin Dunglas - * - * @deprecated - */ - interface FilterInterface - { - /** - * Gets the description of this filter for the given resource. - * - * Returns an array with the filter parameter names as keys and array with the following data as values: - * - property: the property where the filter is applied - * - type: the type of the filter - * - required: if this filter is required - * - strategy (optional): the used strategy - * - is_collection (optional): if this filter is for collection - * - swagger (optional): additional parameters for the path operation, - * e.g. 'swagger' => [ - * 'description' => 'My Description', - * 'name' => 'My Name', - * 'type' => 'integer', - * ] - * - openapi (optional): additional parameters for the path operation in the version 3 spec, - * e.g. 'openapi' => [ - * 'description' => 'My Description', - * 'name' => 'My Name', - * 'schema' => [ - * 'type' => 'integer', - * ] - * ] - * - schema (optional): schema definition, - * e.g. 'schema' => [ - * 'type' => 'string', - * 'enum' => ['value_1', 'value_2'], - * ] - * The description can contain additional data specific to a filter. - * - * @see \ApiPlatform\OpenApi\Factory\OpenApiFactory::getFiltersParameters - */ - public function getDescription(string $resourceClass): array; - } -} diff --git a/src/Api/FilterLocatorTrait.php b/src/Api/FilterLocatorTrait.php deleted file mode 100644 index bc427336c87..00000000000 --- a/src/Api/FilterLocatorTrait.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Exception\InvalidArgumentException; -use Psr\Container\ContainerInterface; - -/** - * Manipulates filters with a backward compatibility between the new filter locator and the deprecated filter collection. - * - * @author Baptiste Meyer - * - * @deprecated - * - * @internal - */ -trait FilterLocatorTrait -{ - private ?ContainerInterface $filterLocator = null; - - /** - * Sets a filter locator with a backward compatibility. - */ - private function setFilterLocator(?ContainerInterface $filterLocator, bool $allowNull = false): void - { - if ($filterLocator instanceof ContainerInterface || (null === $filterLocator && $allowNull)) { - $this->filterLocator = $filterLocator; - } else { - throw new InvalidArgumentException(\sprintf('The "$filterLocator" argument is expected to be an implementation of the "%s" interface%s.', ContainerInterface::class, $allowNull ? ' or null' : '')); - } - } - - /** - * Gets a filter with a backward compatibility. - */ - private function getFilter(string $filterId): ?FilterInterface - { - if ($this->filterLocator && $this->filterLocator->has($filterId)) { - return $this->filterLocator->get($filterId); - } - - return null; - } -} diff --git a/src/Api/FormatMatcher.php b/src/Api/FormatMatcher.php deleted file mode 100644 index 957fd1c7dc5..00000000000 --- a/src/Api/FormatMatcher.php +++ /dev/null @@ -1,64 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -/** - * Matches a mime type to a format. - * - * @internal - */ -final class FormatMatcher -{ - /** - * @var array - */ - private readonly array $formats; - - /** - * @param array $formats - */ - public function __construct(array $formats) - { - $normalizedFormats = []; - foreach ($formats as $format => $mimeTypes) { - $normalizedFormats[$format] = (array) $mimeTypes; - } - $this->formats = $normalizedFormats; - } - - /** - * Gets the format associated with the mime type. - * - * Adapted from {@see \Symfony\Component\HttpFoundation\Request::getFormat}. - */ - public function getFormat(string $mimeType): ?string - { - $canonicalMimeType = null; - $pos = strpos($mimeType, ';'); - if (false !== $pos) { - $canonicalMimeType = trim(substr($mimeType, 0, $pos)); - } - - foreach ($this->formats as $format => $mimeTypes) { - if (\in_array($mimeType, $mimeTypes, true)) { - return $format; - } - if (null !== $canonicalMimeType && \in_array($canonicalMimeType, $mimeTypes, true)) { - return $format; - } - } - - return null; - } -} diff --git a/src/Api/IdentifiersExtractor.php b/src/Api/IdentifiersExtractor.php deleted file mode 100644 index 0db5d0a74a2..00000000000 --- a/src/Api/IdentifiersExtractor.php +++ /dev/null @@ -1,167 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; -use ApiPlatform\Metadata\HttpOperation; -use ApiPlatform\Metadata\Operation; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; -use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; - -/** - * {@inheritdoc} - * - * @deprecated use ApiPlatform\Metadata\IdentifiersExtractor instead - * - * @author Antoine Bluchet - */ -final class IdentifiersExtractor implements IdentifiersExtractorInterface -{ - use ResourceClassInfoTrait; - private readonly PropertyAccessorInterface $propertyAccessor; - - public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ?PropertyAccessorInterface $propertyAccessor = null) - { - $this->resourceMetadataFactory = $resourceMetadataFactory; - $this->resourceClassResolver = $resourceClassResolver; - $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); - } - - /** - * {@inheritdoc} - * - * TODO: 3.0 identifiers should be stringable? - */ - public function getIdentifiersFromItem(object $item, ?Operation $operation = null, array $context = []): array - { - if (!$this->isResourceClass($this->getObjectClass($item))) { - return ['id' => $this->propertyAccessor->getValue($item, 'id')]; - } - - if ($operation && $operation->getClass()) { - return $this->getIdentifiersFromOperation($item, $operation, $context); - } - - $resourceClass = $this->getResourceClass($item, true); - $operation ??= $this->resourceMetadataFactory->create($resourceClass)->getOperation(null, false, true); - - return $this->getIdentifiersFromOperation($item, $operation, $context); - } - - private function getIdentifiersFromOperation(object $item, Operation $operation, array $context = []): array - { - if ($operation instanceof HttpOperation) { - $links = $operation->getUriVariables(); - } elseif ($operation instanceof GraphQlOperation) { - $links = $operation->getLinks(); - } - - $identifiers = []; - foreach ($links ?? [] as $k => $link) { - $linkIdentifiers = $link->getIdentifiers() ?? [$k]; - if (1 < \count($linkIdentifiers)) { - $compositeIdentifiers = []; - foreach ($linkIdentifiers as $identifier) { - $compositeIdentifiers[$identifier] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $identifier, $link->getParameterName()); - } - - $identifiers[$link->getParameterName()] = CompositeIdentifierParser::stringify($compositeIdentifiers); - continue; - } - - $parameterName = $link->getParameterName(); - $identifiers[$parameterName] = $this->getIdentifierValue($item, $link->getFromClass() ?? $operation->getClass(), $linkIdentifiers[0], $parameterName, $link->getToProperty()); - } - - return $identifiers; - } - - /** - * Gets the value of the given class property. - */ - private function getIdentifierValue(object $item, string $class, string $property, string $parameterName, ?string $toProperty = null): float|bool|int|string - { - if ($item instanceof $class) { - try { - return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, $property), $parameterName); - } catch (NoSuchPropertyException $e) { - throw new RuntimeException('Not able to retrieve identifiers.', $e->getCode(), $e); - } - } - - if ($toProperty) { - return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$toProperty.$property"), $parameterName); - } - - $resourceClass = $this->getResourceClass($item, true); - foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) { - $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName); - - $types = $propertyMetadata->getBuiltinTypes(); - if (null === ($type = $types[0] ?? null)) { - continue; - } - - try { - if ($type->isCollection()) { - $collectionValueType = $type->getCollectionValueTypes()[0] ?? null; - - if (null !== $collectionValueType && $collectionValueType->getClassName() === $class) { - return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, \sprintf('%s[0].%s', $propertyName, $property)), $parameterName); - } - } - - if ($type->getClassName() === $class) { - return $this->resolveIdentifierValue($this->propertyAccessor->getValue($item, "$propertyName.$property"), $parameterName); - } - } catch (NoSuchPropertyException $e) { - throw new RuntimeException('Not able to retrieve identifiers.', $e->getCode(), $e); - } - } - - throw new RuntimeException('Not able to retrieve identifiers.'); - } - - /** - * TODO: in 3.0 this method just uses $identifierValue instanceof \Stringable and we remove the weird behavior. - * - * @param mixed|\Stringable $identifierValue - */ - private function resolveIdentifierValue(mixed $identifierValue, string $parameterName): float|bool|int|string - { - if (null === $identifierValue) { - throw new RuntimeException('No identifier value found, did you forget to persist the entity?'); - } - - if (\is_scalar($identifierValue)) { - return $identifierValue; - } - - if ($identifierValue instanceof \Stringable) { - return (string) $identifierValue; - } - - if ($identifierValue instanceof \BackedEnum) { - return (string) $identifierValue->value; - } - - throw new RuntimeException(\sprintf('We were not able to resolve the identifier matching parameter "%s".', $parameterName)); - } -} diff --git a/src/Api/IdentifiersExtractorInterface.php b/src/Api/IdentifiersExtractorInterface.php deleted file mode 100644 index 1ad79b4fd19..00000000000 --- a/src/Api/IdentifiersExtractorInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Metadata\Operation; - -/** - * Extracts identifiers for a given Resource according to the retrieved Metadata. - * - * @author Antoine Bluchet - */ -interface IdentifiersExtractorInterface -{ - /** - * Finds identifiers from an Item (object). - * - * @throws RuntimeException - */ - public function getIdentifiersFromItem(object $item, ?Operation $operation = null, array $context = []): array; -} diff --git a/src/Api/IriConverterInterface.php b/src/Api/IriConverterInterface.php deleted file mode 100644 index b2990f339d2..00000000000 --- a/src/Api/IriConverterInterface.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Exception\InvalidArgumentException; -use ApiPlatform\Metadata\Exception\ItemNotFoundException; -use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Metadata\Operation; - -/** - * Converts item and resources to IRI and vice versa. - * - * @author Kévin Dunglas - */ -interface IriConverterInterface -{ - /** - * Retrieves an item from its IRI. - * - * @throws InvalidArgumentException - * @throws ItemNotFoundException - */ - public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object; - - /** - * Gets the IRI associated with the given item. - * - * @param object|class-string $resource - * - * @throws InvalidArgumentException - * @throws RuntimeException - */ - public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): ?string; -} diff --git a/src/Api/QueryParameterValidator/QueryParameterValidator.php b/src/Api/QueryParameterValidator/QueryParameterValidator.php deleted file mode 100644 index 4c3954865bd..00000000000 --- a/src/Api/QueryParameterValidator/QueryParameterValidator.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator; - -use ApiPlatform\ParameterValidator\ParameterValidator as NewQueryParameterValidator; - -/** - * Validates query parameters depending on filter description. - * - * @deprecated use ApiPlatform\QueryParameterValidator\QueryParameterValidator instead - */ -class QueryParameterValidator extends NewQueryParameterValidator -{ -} diff --git a/src/Api/QueryParameterValidator/Validator/ArrayItems.php b/src/Api/QueryParameterValidator/Validator/ArrayItems.php deleted file mode 100644 index 940d5f77e66..00000000000 --- a/src/Api/QueryParameterValidator/Validator/ArrayItems.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\ArrayItems instead - */ -final class ArrayItems implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - if (!\array_key_exists($name, $queryParameters)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $maxItems = $filterDescription['openapi']['maxItems'] ?? $filterDescription['swagger']['maxItems'] ?? null; - $minItems = $filterDescription['openapi']['minItems'] ?? $filterDescription['swagger']['minItems'] ?? null; - $uniqueItems = $filterDescription['openapi']['uniqueItems'] ?? $filterDescription['swagger']['uniqueItems'] ?? false; - - $errorList = []; - - $value = $this->getValue($name, $filterDescription, $queryParameters); - $nbItems = \count($value); - - if (null !== $maxItems && $nbItems > $maxItems) { - $errorList[] = \sprintf('Query parameter "%s" must contain less than %d values', $name, $maxItems); - } - - if (null !== $minItems && $nbItems < $minItems) { - $errorList[] = \sprintf('Query parameter "%s" must contain more than %d values', $name, $minItems); - } - - if (true === $uniqueItems && $nbItems > \count(array_unique($value))) { - $errorList[] = \sprintf('Query parameter "%s" must contain unique values', $name); - } - - return $errorList; - } - - private function getValue(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - - if (empty($value) && '0' !== $value) { - return []; - } - - if (\is_array($value)) { - return $value; - } - - $collectionFormat = $filterDescription['openapi']['collectionFormat'] ?? $filterDescription['swagger']['collectionFormat'] ?? 'csv'; - - return explode(self::getSeparator($collectionFormat), (string) $value) ?: []; // @phpstan-ignore-line - } - - /** - * @return non-empty-string - */ - private static function getSeparator(string $collectionFormat): string - { - return match ($collectionFormat) { - 'csv' => ',', - 'ssv' => ' ', - 'tsv' => '\t', - 'pipes' => '|', - default => throw new \InvalidArgumentException(\sprintf('Unknown collection format %s', $collectionFormat)), - }; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/Bounds.php b/src/Api/QueryParameterValidator/Validator/Bounds.php deleted file mode 100644 index 0983ccc5d6e..00000000000 --- a/src/Api/QueryParameterValidator/Validator/Bounds.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\Bounds instead - */ -final class Bounds implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $maximum = $filterDescription['openapi']['maximum'] ?? $filterDescription['swagger']['maximum'] ?? null; - $minimum = $filterDescription['openapi']['minimum'] ?? $filterDescription['swagger']['minimum'] ?? null; - - $errorList = []; - - if (null !== $maximum) { - if (($filterDescription['openapi']['exclusiveMaximum'] ?? $filterDescription['swagger']['exclusiveMaximum'] ?? false) && $value >= $maximum) { - $errorList[] = \sprintf('Query parameter "%s" must be less than %s', $name, $maximum); - } elseif ($value > $maximum) { - $errorList[] = \sprintf('Query parameter "%s" must be less than or equal to %s', $name, $maximum); - } - } - - if (null !== $minimum) { - if (($filterDescription['openapi']['exclusiveMinimum'] ?? $filterDescription['swagger']['exclusiveMinimum'] ?? false) && $value <= $minimum) { - $errorList[] = \sprintf('Query parameter "%s" must be greater than %s', $name, $minimum); - } elseif ($value < $minimum) { - $errorList[] = \sprintf('Query parameter "%s" must be greater than or equal to %s', $name, $minimum); - } - } - - return $errorList; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/Enum.php b/src/Api/QueryParameterValidator/Validator/Enum.php deleted file mode 100644 index 53a1c2c1b9a..00000000000 --- a/src/Api/QueryParameterValidator/Validator/Enum.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\Enum instead - */ -final class Enum implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $enum = $filterDescription['openapi']['enum'] ?? $filterDescription['swagger']['enum'] ?? null; - - if (null !== $enum && !\in_array($value, $enum, true)) { - return [ - \sprintf('Query parameter "%s" must be one of "%s"', $name, implode(', ', $enum)), - ]; - } - - return []; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/Length.php b/src/Api/QueryParameterValidator/Validator/Length.php deleted file mode 100644 index 9286cdb1975..00000000000 --- a/src/Api/QueryParameterValidator/Validator/Length.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\Length instead - */ -final class Length implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $maxLength = $filterDescription['openapi']['maxLength'] ?? $filterDescription['swagger']['maxLength'] ?? null; - $minLength = $filterDescription['openapi']['minLength'] ?? $filterDescription['swagger']['minLength'] ?? null; - - $errorList = []; - - if (null !== $maxLength && mb_strlen($value) > $maxLength) { - $errorList[] = \sprintf('Query parameter "%s" length must be lower than or equal to %s', $name, $maxLength); - } - - if (null !== $minLength && mb_strlen($value) < $minLength) { - $errorList[] = \sprintf('Query parameter "%s" length must be greater than or equal to %s', $name, $minLength); - } - - return $errorList; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/MultipleOf.php b/src/Api/QueryParameterValidator/Validator/MultipleOf.php deleted file mode 100644 index 77e48ef17af..00000000000 --- a/src/Api/QueryParameterValidator/Validator/MultipleOf.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\MultipleOf instead - */ -final class MultipleOf implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $multipleOf = $filterDescription['openapi']['multipleOf'] ?? $filterDescription['swagger']['multipleOf'] ?? null; - - if (null !== $multipleOf && 0 !== ($value % $multipleOf)) { - return [ - \sprintf('Query parameter "%s" must multiple of %s', $name, $multipleOf), - ]; - } - - return []; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/Pattern.php b/src/Api/QueryParameterValidator/Validator/Pattern.php deleted file mode 100644 index 8d134761042..00000000000 --- a/src/Api/QueryParameterValidator/Validator/Pattern.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\Pattern instead - */ -final class Pattern implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - $value = $queryParameters[$name] ?? null; - if (empty($value) && '0' !== $value || !\is_string($value)) { - return []; - } - - $this->checkFilterDeprecations($filterDescription); - - $pattern = $filterDescription['openapi']['pattern'] ?? $filterDescription['swagger']['pattern'] ?? null; - - if (null !== $pattern && !preg_match($pattern, $value)) { - return [ - \sprintf('Query parameter "%s" must match pattern %s', $name, $pattern), - ]; - } - - return []; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/Required.php b/src/Api/QueryParameterValidator/Validator/Required.php deleted file mode 100644 index bc8c02fffd1..00000000000 --- a/src/Api/QueryParameterValidator/Validator/Required.php +++ /dev/null @@ -1,111 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator\CheckFilterDeprecationsTrait; -use ApiPlatform\ParameterValidator\Validator\ValidatorInterface; -use ApiPlatform\State\Util\RequestParser; - -/** - * @deprecated use \ApiPlatform\ParameterValidator\Validator\Required instead - */ -final class Required implements ValidatorInterface -{ - use CheckFilterDeprecationsTrait; - - /** - * {@inheritdoc} - */ - public function validate(string $name, array $filterDescription, array $queryParameters): array - { - // filter is not required, the `checkRequired` method can not break - if (!($filterDescription['required'] ?? false)) { - return []; - } - - // if query param is not given, then break - if (!$this->requestHasQueryParameter($queryParameters, $name)) { - return [ - \sprintf('Query parameter "%s" is required', $name), - ]; - } - - $this->checkFilterDeprecations($filterDescription); - - // if query param is empty and the configuration does not allow it - if (!($filterDescription['openapi']['allowEmptyValue'] ?? $filterDescription['swagger']['allowEmptyValue'] ?? false) && empty($this->requestGetQueryParameter($queryParameters, $name))) { - return [ - \sprintf('Query parameter "%s" does not allow empty value', $name), - ]; - } - - return []; - } - - /** - * Test if request has required parameter. - */ - private function requestHasQueryParameter(array $queryParameters, string $name): bool - { - $matches = RequestParser::parseRequestParams($name); - if (!$matches) { - return false; - } - - $rootName = array_keys($matches)[0] ?? ''; - if (!$rootName) { - return false; - } - - if (\is_array($matches[$rootName])) { - $keyName = array_keys($matches[$rootName])[0]; - - $queryParameter = $queryParameters[(string) $rootName] ?? null; - - return \is_array($queryParameter) && isset($queryParameter[$keyName]); - } - - return \array_key_exists((string) $rootName, $queryParameters); - } - - /** - * Test if required filter is valid. It validates array notation too like "required[bar]". - */ - private function requestGetQueryParameter(array $queryParameters, string $name) - { - $matches = RequestParser::parseRequestParams($name); - if (empty($matches)) { - return null; - } - - $rootName = array_keys($matches)[0] ?? ''; - if (!$rootName) { - return null; - } - - if (\is_array($matches[$rootName])) { - $keyName = array_keys($matches[$rootName])[0]; - - $queryParameter = $queryParameters[(string) $rootName] ?? null; - - if (\is_array($queryParameter) && isset($queryParameter[$keyName])) { - return $queryParameter[$keyName]; - } - - return null; - } - - return $queryParameters[(string) $rootName]; - } -} diff --git a/src/Api/QueryParameterValidator/Validator/ValidatorInterface.php b/src/Api/QueryParameterValidator/Validator/ValidatorInterface.php deleted file mode 100644 index 2bb50512048..00000000000 --- a/src/Api/QueryParameterValidator/Validator/ValidatorInterface.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\QueryParameterValidator\Validator; - -use ApiPlatform\ParameterValidator\Validator as ParameterValidatorComponent; - -/** @deprecated use \ApiPlatform\ParameterValidator\Validator\ValidatorInterface instead */ -interface ValidatorInterface extends ParameterValidatorComponent\ValidatorInterface -{ -} diff --git a/src/Api/ResourceClassResolver.php b/src/Api/ResourceClassResolver.php deleted file mode 100644 index 03b8781e67a..00000000000 --- a/src/Api/ResourceClassResolver.php +++ /dev/null @@ -1,110 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; - -/** - * {@inheritdoc} - * - * @deprecated replaced by ApiPlatform\Metadata\ResourceClassResolver - * - * @author Kévin Dunglas - * @author Samuel ROZE - */ -final class ResourceClassResolver implements ResourceClassResolverInterface -{ - use ClassInfoTrait; - private array $localIsResourceClassCache = []; - private array $localMostSpecificResourceClassCache = []; - - public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory) - { - } - - /** - * {@inheritdoc} - */ - public function getResourceClass(mixed $value, ?string $resourceClass = null, bool $strict = false): string - { - if ($strict && null === $resourceClass) { - throw new InvalidArgumentException('Strict checking is only possible when resource class is specified.'); - } - - $objectClass = \is_object($value) ? $this->getObjectClass($value) : null; - $actualClass = ($objectClass && (!$value instanceof \Traversable || $this->isResourceClass($objectClass))) ? $this->getObjectClass($value) : null; - - if (null === $actualClass && null === $resourceClass) { - throw new InvalidArgumentException('Resource type could not be determined. Resource class must be specified.'); - } - - if (null !== $actualClass && !$this->isResourceClass($actualClass)) { - throw new InvalidArgumentException(\sprintf('No resource class found for object of type "%s".', $actualClass)); - } - - if (null !== $resourceClass && !$this->isResourceClass($resourceClass)) { - throw new InvalidArgumentException(\sprintf('Specified class "%s" is not a resource class.', $resourceClass)); - } - - if ($strict && null !== $actualClass && !is_a($actualClass, $resourceClass, true)) { - throw new InvalidArgumentException(\sprintf('Object of type "%s" does not match "%s" resource class.', $actualClass, $resourceClass)); - } - - $targetClass = $actualClass ?? $resourceClass; - - if (isset($this->localMostSpecificResourceClassCache[$targetClass])) { - return $this->localMostSpecificResourceClassCache[$targetClass]; - } - - $mostSpecificResourceClass = null; - - foreach ($this->resourceNameCollectionFactory->create() as $resourceClassName) { - if (!is_a($targetClass, $resourceClassName, true)) { - continue; - } - - if (null === $mostSpecificResourceClass || is_subclass_of($resourceClassName, $mostSpecificResourceClass)) { - $mostSpecificResourceClass = $resourceClassName; - } - } - - if (null === $mostSpecificResourceClass) { - throw new \LogicException('Unexpected execution flow.'); - } - - $this->localMostSpecificResourceClassCache[$targetClass] = $mostSpecificResourceClass; - - return $mostSpecificResourceClass; - } - - /** - * {@inheritdoc} - */ - public function isResourceClass(string $type): bool - { - if (isset($this->localIsResourceClassCache[$type])) { - return $this->localIsResourceClassCache[$type]; - } - - foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) { - if (is_a($type, $resourceClass, true)) { - return $this->localIsResourceClassCache[$type] = true; - } - } - - return $this->localIsResourceClassCache[$type] = false; - } -} diff --git a/src/Api/ResourceClassResolverInterface.php b/src/Api/ResourceClassResolverInterface.php deleted file mode 100644 index 75e6a021d64..00000000000 --- a/src/Api/ResourceClassResolverInterface.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Exception\InvalidArgumentException; - -/** - * Guesses which resource is associated with a given object. - * - * @author Kévin Dunglas - */ -interface ResourceClassResolverInterface -{ - /** - * Guesses the associated resource. - * - * @param string $resourceClass The expected resource class - * @param bool $strict If true, value must match the expected resource class - * - * @throws InvalidArgumentException - */ - public function getResourceClass(mixed $value, ?string $resourceClass = null, bool $strict = false): string; - - /** - * Is the given class a resource class? - */ - public function isResourceClass(string $type): bool; -} diff --git a/src/Api/UriVariableTransformer/IntegerUriVariableTransformer.php b/src/Api/UriVariableTransformer/IntegerUriVariableTransformer.php deleted file mode 100644 index 9c649427a50..00000000000 --- a/src/Api/UriVariableTransformer/IntegerUriVariableTransformer.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api\UriVariableTransformer; - -use ApiPlatform\Api\UriVariableTransformerInterface; -use Symfony\Component\PropertyInfo\Type; - -final class IntegerUriVariableTransformer implements UriVariableTransformerInterface -{ - public function transform(mixed $value, array $types, array $context = []): int - { - return (int) $value; - } - - public function supportsTransformation(mixed $value, array $types, array $context = []): bool - { - return Type::BUILTIN_TYPE_INT === $types[0] && \is_string($value); - } -} diff --git a/src/Api/UriVariableTransformerInterface.php b/src/Api/UriVariableTransformerInterface.php deleted file mode 100644 index c3a0013244e..00000000000 --- a/src/Api/UriVariableTransformerInterface.php +++ /dev/null @@ -1,39 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Exception\InvalidUriVariableException; - -interface UriVariableTransformerInterface -{ - /** - * Transforms the value of a URI variable (identifier) to its type. - * - * @param mixed $value The URI variable value to transform - * @param array $types The guessed type behind the URI variable - * @param array $context Options available to the transformer - * - * @throws InvalidUriVariableException Occurs when the URI variable could not be transformed - */ - public function transform(mixed $value, array $types, array $context = []); - - /** - * Checks whether the value of a URI variable can be transformed to its type by this transformer. - * - * @param mixed $value The URI variable value to transform - * @param array $types The types to which the URI variable value should be transformed - * @param array $context Options available to the transformer - */ - public function supportsTransformation(mixed $value, array $types, array $context = []): bool; -} diff --git a/src/Api/UriVariablesConverter.php b/src/Api/UriVariablesConverter.php deleted file mode 100644 index 59999547cdc..00000000000 --- a/src/Api/UriVariablesConverter.php +++ /dev/null @@ -1,91 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Exception\InvalidUriVariableException; -use ApiPlatform\Metadata\Link; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use Symfony\Component\PropertyInfo\Type; - -/** - * UriVariables converter that chains uri variables transformers. - * - * @author Antoine Bluchet - */ -final class UriVariablesConverter implements UriVariablesConverterInterface -{ - /** - * @param iterable $uriVariableTransformers - */ - public function __construct(private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly iterable $uriVariableTransformers) - { - } - - /** - * {@inheritdoc} - * - * To handle the composite identifiers type correctly, use an `uri_variables_map` that maps uriVariables to their uriVariablesDefinition. - * Indeed, a composite identifier will already be parsed, and their corresponding properties will be the parameterName and not the defined - * identifiers. - */ - public function convert(array $uriVariables, string $class, array $context = []): array - { - $operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($class)->getOperation(); - $context += ['operation' => $operation]; - $uriVariablesDefinitions = $operation->getUriVariables() ?? []; - - foreach ($uriVariables as $parameterName => $value) { - $uriVariableDefinition = $context['uri_variables_map'][$parameterName] ?? $uriVariablesDefinitions[$parameterName] ?? $uriVariablesDefinitions['id'] ?? new Link(); - - // When a composite identifier is used, we assume that the parameterName is the property to find our type - $properties = $uriVariableDefinition->getIdentifiers() ?? [$parameterName]; - if ($uriVariableDefinition->getCompositeIdentifier()) { - $properties = [$parameterName]; - } - - if (!$types = $this->getIdentifierTypes($uriVariableDefinition->getFromClass() ?? $class, $properties)) { - continue; - } - - foreach ($this->uriVariableTransformers as $uriVariableTransformer) { - if (!$uriVariableTransformer->supportsTransformation($value, $types, $context)) { - continue; - } - - try { - $uriVariables[$parameterName] = $uriVariableTransformer->transform($value, $types, $context); - break; - } catch (InvalidUriVariableException $e) { - throw new InvalidUriVariableException(\sprintf('Identifier "%s" could not be transformed.', $parameterName), $e->getCode(), $e); - } - } - } - - return $uriVariables; - } - - private function getIdentifierTypes(string $resourceClass, array $properties): array - { - $types = []; - foreach ($properties as $property) { - $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property); - foreach ($propertyMetadata->getBuiltinTypes() as $type) { - $types[] = Type::BUILTIN_TYPE_OBJECT === ($builtinType = $type->getBuiltinType()) ? $type->getClassName() : $builtinType; - } - } - - return $types; - } -} diff --git a/src/Api/UriVariablesConverterInterface.php b/src/Api/UriVariablesConverterInterface.php deleted file mode 100644 index 67c49092221..00000000000 --- a/src/Api/UriVariablesConverterInterface.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use ApiPlatform\Metadata\Exception\InvalidIdentifierException; - -/** - * Identifier converter. - * - * @author Antoine Bluchet - */ -interface UriVariablesConverterInterface -{ - /** - * Takes an array of strings representing URI variables (identifiers) and transform their values to the expected type. - * - * @param array $data URI variables to convert to PHP values - * @param string $class The class to which the URI variables belong to - * - * @throws InvalidIdentifierException - * - * @return array Array indexed by identifiers properties with their values denormalized - */ - public function convert(array $data, string $class, array $context = []): array; -} diff --git a/src/Api/UrlGeneratorInterface.php b/src/Api/UrlGeneratorInterface.php deleted file mode 100644 index b6ee97cf0cd..00000000000 --- a/src/Api/UrlGeneratorInterface.php +++ /dev/null @@ -1,84 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Api; - -use Symfony\Component\Routing\Exception\InvalidParameterException; -use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; -use Symfony\Component\Routing\Exception\RouteNotFoundException; - -/** - * UrlGeneratorInterface is the interface that all URL generator classes must implement. - * - * This interface has been imported and adapted from the Symfony project. - * - * The constants in this interface define the different types of resource references that - * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986 - * We are using the term "URL" instead of "URI" as this is more common in web applications - * and we do not need to distinguish them as the difference is mostly semantical and - * less technical. Generating URIs, i.e. representation-independent resource identifiers, - * is also possible. - * - * @author Fabien Potencier - * @author Tobias Schultze - * @copyright Fabien Potencier - */ -interface UrlGeneratorInterface -{ - /** - * Generates an absolute URL, e.g. "http://example.com/dir/file". - */ - public const ABS_URL = 0; - - /** - * Generates an absolute path, e.g. "/dir/file". - */ - public const ABS_PATH = 1; - - /** - * Generates a relative path based on the current request path, e.g. "../parent-file". - * - * @see UrlGenerator::getRelativePath() - */ - public const REL_PATH = 2; - - /** - * Generates a network path, e.g. "//example.com/dir/file". - * Such reference reuses the current scheme but specifies the host. - */ - public const NET_PATH = 3; - - /** - * Generates a URL or path for a specific route based on the given parameters. - * - * Parameters that reference placeholders in the route pattern will substitute them in the - * path or host. Extra params are added as query string to the URL. - * - * When the passed reference type cannot be generated for the route because it requires a different - * host or scheme than the current one, the method will return a more comprehensive reference - * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH - * but the route requires the https scheme whereas the current scheme is http, it will instead return an - * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches - * the route in any case. - * - * If there is no route with the given name, the generator must throw the RouteNotFoundException. - * - * The special parameter _fragment will be used as the document fragment suffixed to the final URL. - * - * @throws RouteNotFoundException If the named route doesn't exist - * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route - * @throws InvalidParameterException When a parameter value for a placeholder is not correct because - * it does not match the requirement - */ - public function generate(string $name, array $parameters = [], int $referenceType = self::ABS_PATH): string; -} diff --git a/src/Doctrine/Common/.gitattributes b/src/Doctrine/Common/.gitattributes index ae3c2e1685a..801f2080d71 100644 --- a/src/Doctrine/Common/.gitattributes +++ b/src/Doctrine/Common/.gitattributes @@ -1,2 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore /.gitignore export-ignore /Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/Doctrine/Common/.github/workflows/close_pr.yml b/src/Doctrine/Common/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/Doctrine/Common/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/Doctrine/Common/CollectionPaginator.php b/src/Doctrine/Common/CollectionPaginator.php index c4127280b58..93d56abd1d6 100644 --- a/src/Doctrine/Common/CollectionPaginator.php +++ b/src/Doctrine/Common/CollectionPaginator.php @@ -34,7 +34,7 @@ final class CollectionPaginator implements \IteratorAggregate, PaginatorInterfac * @param ReadableCollection $collection */ public function __construct( - readonly ReadableCollection $collection, + public readonly ReadableCollection $collection, private readonly float $currentPage, private readonly float $itemsPerPage, ) { diff --git a/src/Doctrine/Common/Filter/BackedEnumFilterTrait.php b/src/Doctrine/Common/Filter/BackedEnumFilterTrait.php new file mode 100644 index 00000000000..811011e805b --- /dev/null +++ b/src/Doctrine/Common/Filter/BackedEnumFilterTrait.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Filter; + +use ApiPlatform\Doctrine\Common\PropertyHelperTrait; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use Psr\Log\LoggerInterface; + +/** + * Trait for filtering the collection by backed enum values. + * + * Filters collection on equality of backed enum properties. + * + * For each property passed, if the resource does not have such property or if + * the value is not one of cases the property is ignored. + * + * @author Rémi Marseille + */ +trait BackedEnumFilterTrait +{ + use PropertyHelperTrait; + + /** + * @var array + */ + private array $enumTypes; + + /** + * {@inheritdoc} + */ + public function getDescription(string $resourceClass): array + { + $description = []; + + $properties = $this->getProperties(); + if (null === $properties) { + $properties = array_fill_keys($this->getClassMetadata($resourceClass)->getFieldNames(), null); + } + + foreach ($properties as $property => $unused) { + if (!$this->isPropertyMapped($property, $resourceClass) || !$this->isBackedEnumField($property, $resourceClass)) { + continue; + } + $propertyName = $this->normalizePropertyName($property); + $filterParameterNames = [$propertyName]; + $filterParameterNames[] = $propertyName.'[]'; + + foreach ($filterParameterNames as $filterParameterName) { + $isCollection = str_ends_with($filterParameterName, '[]'); + + $enumValues = array_map(fn (\BackedEnum $case) => $case->value, $this->enumTypes[$property]::cases()); + + $schema = $isCollection + ? ['type' => 'array', 'items' => ['type' => 'string', 'enum' => $enumValues]] + : ['type' => 'string', 'enum' => $enumValues]; + + $description[$filterParameterName] = [ + 'property' => $propertyName, + 'type' => 'string', + 'required' => false, + 'is_collection' => $isCollection, + 'schema' => $schema, + ]; + } + } + + return $description; + } + + abstract protected function getProperties(): ?array; + + abstract protected function getLogger(): LoggerInterface; + + abstract protected function normalizePropertyName(string $property): string; + + /** + * Determines whether the given property refers to a backed enum field. + */ + abstract protected function isBackedEnumField(string $property, string $resourceClass): bool; + + private function normalizeValue(mixed $value, string $property): mixed + { + $firstCase = $this->enumTypes[$property]::cases()[0] ?? null; + if ( + \is_int($firstCase?->value) + && false !== filter_var($value, \FILTER_VALIDATE_INT) + ) { + $value = (int) $value; + } + + $values = array_map(fn (\BackedEnum $case) => $case->value, $this->enumTypes[$property]::cases()); + + if (\in_array($value, $values, true)) { + return $value; + } + + $this->getLogger()->notice('Invalid filter ignored', [ + 'exception' => new InvalidArgumentException(\sprintf('Invalid backed enum value for "%s" property, expected one of ( "%s" )', + $property, + implode('" | "', $values) + )), + ]); + + return null; + } +} diff --git a/src/Doctrine/Common/Filter/BooleanFilterTrait.php b/src/Doctrine/Common/Filter/BooleanFilterTrait.php index c9ee4a364d8..4e6a42cff31 100644 --- a/src/Doctrine/Common/Filter/BooleanFilterTrait.php +++ b/src/Doctrine/Common/Filter/BooleanFilterTrait.php @@ -75,7 +75,7 @@ protected function isBooleanField(string $property, string $resourceClass): bool return isset(self::DOCTRINE_BOOLEAN_TYPES[(string) $this->getDoctrineFieldType($property, $resourceClass)]); } - private function normalizeValue($value, string $property): ?bool + private function normalizeValue(mixed $value, string $property): ?bool { if (\in_array($value, [true, 'true', '1'], true)) { return true; diff --git a/src/Doctrine/Common/Filter/DateFilterTrait.php b/src/Doctrine/Common/Filter/DateFilterTrait.php index 33f24664459..4485e94881a 100644 --- a/src/Doctrine/Common/Filter/DateFilterTrait.php +++ b/src/Doctrine/Common/Filter/DateFilterTrait.php @@ -81,7 +81,7 @@ protected function getFilterDescription(string $property, string $period): array ]; } - private function normalizeValue($value, string $operator): ?string + private function normalizeValue(mixed $value, string $operator): ?string { if (false === \is_string($value)) { $this->getLogger()->notice('Invalid filter ignored', [ @@ -91,6 +91,14 @@ private function normalizeValue($value, string $operator): ?string return null; } + if ('' === $value) { + $this->getLogger()->notice('Invalid filter ignored', [ + 'exception' => new InvalidArgumentException(\sprintf('Invalid value for "[%s]", expected non-empty string', $operator)), + ]); + + return null; + } + return $value; } } diff --git a/src/Doctrine/Common/Filter/ExistsFilterTrait.php b/src/Doctrine/Common/Filter/ExistsFilterTrait.php index c1ab4a3e3a2..3521daf98ef 100644 --- a/src/Doctrine/Common/Filter/ExistsFilterTrait.php +++ b/src/Doctrine/Common/Filter/ExistsFilterTrait.php @@ -70,7 +70,7 @@ abstract protected function getLogger(): LoggerInterface; abstract protected function normalizePropertyName(string $property): string; - private function normalizeValue($value, string $property): ?bool + private function normalizeValue(mixed $value, string $property): ?bool { if (\in_array($value, [true, 'true', '1', '', null], true)) { return true; diff --git a/src/Doctrine/Common/Filter/LoggerAwareInterface.php b/src/Doctrine/Common/Filter/LoggerAwareInterface.php new file mode 100644 index 00000000000..aa765210b46 --- /dev/null +++ b/src/Doctrine/Common/Filter/LoggerAwareInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Filter; + +use Psr\Log\LoggerInterface; + +interface LoggerAwareInterface +{ + public function hasLogger(): bool; + + public function getLogger(): LoggerInterface; + + public function setLogger(LoggerInterface $logger): void; +} diff --git a/src/Doctrine/Common/Filter/LoggerAwareTrait.php b/src/Doctrine/Common/Filter/LoggerAwareTrait.php new file mode 100644 index 00000000000..9ed56d1ba6e --- /dev/null +++ b/src/Doctrine/Common/Filter/LoggerAwareTrait.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Filter; + +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; + +trait LoggerAwareTrait +{ + private ?LoggerInterface $logger = null; + + public function hasLogger(): bool + { + return $this->logger instanceof LoggerInterface; + } + + public function getLogger(): LoggerInterface + { + return $this->logger ??= new NullLogger(); + } + + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } +} diff --git a/src/Doctrine/Common/Filter/ManagerRegistryAwareInterface.php b/src/Doctrine/Common/Filter/ManagerRegistryAwareInterface.php new file mode 100644 index 00000000000..c4a6da3affb --- /dev/null +++ b/src/Doctrine/Common/Filter/ManagerRegistryAwareInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Filter; + +use Doctrine\Persistence\ManagerRegistry; + +interface ManagerRegistryAwareInterface +{ + public function hasManagerRegistry(): bool; + + public function getManagerRegistry(): ManagerRegistry; + + public function setManagerRegistry(ManagerRegistry $managerRegistry): void; +} diff --git a/src/Doctrine/Common/Filter/ManagerRegistryAwareTrait.php b/src/Doctrine/Common/Filter/ManagerRegistryAwareTrait.php new file mode 100644 index 00000000000..c7547ac71c2 --- /dev/null +++ b/src/Doctrine/Common/Filter/ManagerRegistryAwareTrait.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Filter; + +use ApiPlatform\Metadata\Exception\RuntimeException; +use Doctrine\Persistence\ManagerRegistry; + +trait ManagerRegistryAwareTrait +{ + private ?ManagerRegistry $managerRegistry = null; + + public function hasManagerRegistry(): bool + { + return $this->managerRegistry instanceof ManagerRegistry; + } + + public function getManagerRegistry(): ManagerRegistry + { + if (!$this->hasManagerRegistry()) { + throw new RuntimeException('ManagerRegistry must be initialized before accessing it.'); + } + + return $this->managerRegistry; + } + + public function setManagerRegistry(ManagerRegistry $managerRegistry): void + { + $this->managerRegistry = $managerRegistry; + } +} diff --git a/src/Doctrine/Common/Filter/NumericFilterTrait.php b/src/Doctrine/Common/Filter/NumericFilterTrait.php index 242a627531d..19fba0b2d00 100644 --- a/src/Doctrine/Common/Filter/NumericFilterTrait.php +++ b/src/Doctrine/Common/Filter/NumericFilterTrait.php @@ -79,7 +79,7 @@ protected function isNumericField(string $property, string $resourceClass): bool return isset(self::DOCTRINE_NUMERIC_TYPES[(string) $this->getDoctrineFieldType($property, $resourceClass)]); } - protected function normalizeValues($value, string $property): ?array + protected function normalizeValues(mixed $value, string $property): ?array { if (!is_numeric($value) && (!\is_array($value) || !$this->isNumericArray($value))) { $this->getLogger()->notice('Invalid filter ignored', [ diff --git a/src/Doctrine/Common/Filter/OpenApiFilterTrait.php b/src/Doctrine/Common/Filter/OpenApiFilterTrait.php new file mode 100644 index 00000000000..7df7e8b9cfb --- /dev/null +++ b/src/Doctrine/Common/Filter/OpenApiFilterTrait.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Filter; + +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; + +/** + * @author Vincent Amstoutz + */ +trait OpenApiFilterTrait +{ + public function getOpenApiParameters(Parameter $parameter): OpenApiParameter|array|null + { + return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true); + } +} diff --git a/src/Doctrine/Common/Filter/OrderFilterTrait.php b/src/Doctrine/Common/Filter/OrderFilterTrait.php index 54e87cf6c1d..2c9258ad487 100644 --- a/src/Doctrine/Common/Filter/OrderFilterTrait.php +++ b/src/Doctrine/Common/Filter/OrderFilterTrait.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Doctrine\Common\Filter; use ApiPlatform\Doctrine\Common\PropertyHelperTrait; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; /** * Trait for ordering the collection by given properties. @@ -54,6 +55,7 @@ public function getDescription(string $resourceClass): array 'required' => false, 'schema' => [ 'type' => 'string', + 'default' => strtolower($propertyOptions['default_direction'] ?? OrderFilterInterface::DIRECTION_ASC), 'enum' => [ strtolower(OrderFilterInterface::DIRECTION_ASC), strtolower(OrderFilterInterface::DIRECTION_DESC), @@ -69,13 +71,21 @@ abstract protected function getProperties(): ?array; abstract protected function normalizePropertyName(string $property): string; - private function normalizeValue($value, string $property): ?string + private function normalizeValue(mixed $value, string $property): ?string { if (empty($value) && null !== $defaultDirection = $this->getProperties()[$property]['default_direction'] ?? null) { // fallback to default direction $value = $defaultDirection; } + if (!\is_string($value)) { + $this->getLogger()->notice('Invalid filter ignored', [ + 'exception' => new InvalidArgumentException(\sprintf('Invalid string value for "%s" property', $property)), + ]); + + return null; + } + $value = strtoupper($value); if (!\in_array($value, [self::DIRECTION_ASC, self::DIRECTION_DESC], true)) { return null; diff --git a/src/Doctrine/Common/Filter/PropertyPlaceholderOpenApiParameterTrait.php b/src/Doctrine/Common/Filter/PropertyPlaceholderOpenApiParameterTrait.php new file mode 100644 index 00000000000..f2f09e5758c --- /dev/null +++ b/src/Doctrine/Common/Filter/PropertyPlaceholderOpenApiParameterTrait.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Filter; + +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; + +trait PropertyPlaceholderOpenApiParameterTrait +{ + /** + * @return array|null + */ + public function getOpenApiParameters(Parameter $parameter): ?array + { + if (str_contains($parameter->getKey(), ':property')) { + $parameters = []; + $key = str_replace('[:property]', '', $parameter->getKey()); + foreach (array_keys($parameter->getExtraProperties()['_properties'] ?? []) as $property) { + $parameters[] = new OpenApiParameter(name: \sprintf('%s[%s]', $key, $property), in: 'query'); + } + + return $parameters; + } + + return null; + } +} diff --git a/src/Doctrine/Common/Filter/SearchFilterTrait.php b/src/Doctrine/Common/Filter/SearchFilterTrait.php index d04fe78ba19..bf64d45370d 100644 --- a/src/Doctrine/Common/Filter/SearchFilterTrait.php +++ b/src/Doctrine/Common/Filter/SearchFilterTrait.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Doctrine\Common\Filter; -use ApiPlatform\Api\IdentifiersExtractorInterface as LegacyIdentifiersExtractorInterface; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Doctrine\Common\PropertyHelperTrait; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\IdentifiersExtractorInterface; @@ -32,9 +30,9 @@ trait SearchFilterTrait { use PropertyHelperTrait; - protected IriConverterInterface|LegacyIriConverterInterface $iriConverter; + protected IriConverterInterface $iriConverter; protected PropertyAccessorInterface $propertyAccessor; - protected IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface|null $identifiersExtractor = null; + protected ?IdentifiersExtractorInterface $identifiersExtractor = null; /** * {@inheritdoc} @@ -111,7 +109,7 @@ abstract protected function getProperties(): ?array; abstract protected function getLogger(): LoggerInterface; - abstract protected function getIriConverter(): LegacyIriConverterInterface|IriConverterInterface; + abstract protected function getIriConverter(): IriConverterInterface; abstract protected function getPropertyAccessor(): PropertyAccessorInterface; diff --git a/src/Doctrine/Common/Messenger/DispatchTrait.php b/src/Doctrine/Common/Messenger/DispatchTrait.php new file mode 100644 index 00000000000..ece731d42ec --- /dev/null +++ b/src/Doctrine/Common/Messenger/DispatchTrait.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Common\Messenger; + +use Symfony\Component\Messenger\Envelope; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\MessageBusInterface; + +/** + * @internal + */ +trait DispatchTrait +{ + private ?MessageBusInterface $messageBus; + + /** + * @param object|Envelope $message + */ + private function dispatch(object $message): Envelope + { + if (!$this->messageBus instanceof MessageBusInterface) { + throw new \InvalidArgumentException('The message bus is not set.'); + } + + if (!class_exists(HandlerFailedException::class)) { + return $this->messageBus->dispatch($message); + } + + try { + return $this->messageBus->dispatch($message); + } catch (HandlerFailedException $e) { + // unwrap the exception thrown in handler for Symfony Messenger >= 4.3 + while ($e instanceof HandlerFailedException) { + /** @var \Throwable $e */ + $e = $e->getPrevious(); + } + + throw $e; + } + } +} diff --git a/src/Doctrine/Common/README.md b/src/Doctrine/Common/README.md index b4ad5264133..b06ba7ff330 100644 --- a/src/Doctrine/Common/README.md +++ b/src/Doctrine/Common/README.md @@ -1,7 +1,12 @@ -# API Platform - Doctrine Common +# API Platform - Doctrine Common Support -Common files used by api-platform/doctrine-orm and api-platform/doctrine-odm - -## Resources +Integration for [Doctrine](https://www.doctrine-project.org) with the [API Platform](https://api-platform.com) framework. +[Documentation](https://api-platform.com/docs/core/getting-started/) +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/Doctrine/Common/SelectablePaginator.php b/src/Doctrine/Common/SelectablePaginator.php index aefa1e17683..0c0ee03391c 100644 --- a/src/Doctrine/Common/SelectablePaginator.php +++ b/src/Doctrine/Common/SelectablePaginator.php @@ -36,7 +36,7 @@ final class SelectablePaginator implements \IteratorAggregate, PaginatorInterfac * @param Selectable $selectable */ public function __construct( - readonly Selectable $selectable, + public readonly Selectable $selectable, private readonly float $currentPage, private readonly float $itemsPerPage, ) { diff --git a/src/Doctrine/Common/SelectablePartialPaginator.php b/src/Doctrine/Common/SelectablePartialPaginator.php index aa01d96a2fd..fbf6170413e 100644 --- a/src/Doctrine/Common/SelectablePartialPaginator.php +++ b/src/Doctrine/Common/SelectablePartialPaginator.php @@ -35,7 +35,7 @@ final class SelectablePartialPaginator implements \IteratorAggregate, PartialPag * @param Selectable $selectable */ public function __construct( - readonly Selectable $selectable, + public readonly Selectable $selectable, private readonly float $currentPage, private readonly float $itemsPerPage, ) { diff --git a/src/Doctrine/Common/State/LinksHandlerLocatorTrait.php b/src/Doctrine/Common/State/LinksHandlerLocatorTrait.php index 77a5330d474..ba4c5d0c5e0 100644 --- a/src/Doctrine/Common/State/LinksHandlerLocatorTrait.php +++ b/src/Doctrine/Common/State/LinksHandlerLocatorTrait.php @@ -35,10 +35,33 @@ private function getLinksHandler(Operation $operation): ?callable return $handleLinks; } + if (\is_array($handleLinks) && 2 === \count($handleLinks) && class_exists($handleLinks[0])) { + [$className, $methodName] = $handleLinks; + + if (method_exists($className, $methodName)) { + return $handleLinks; + } + + $suggestedMethod = $this->findSimilarMethod($className, $methodName); + + throw new RuntimeException(\sprintf('Method "%s" does not exist in class "%s".%s', $methodName, $className, $suggestedMethod ? \sprintf(' Did you mean "%s"?', $suggestedMethod) : '')); + } + if ($this->handleLinksLocator && \is_string($handleLinks) && $this->handleLinksLocator->has($handleLinks)) { return [$this->handleLinksLocator->get($handleLinks), 'handleLinks']; } throw new RuntimeException(\sprintf('Could not find handleLinks service "%s"', $handleLinks)); } + + private function findSimilarMethod(string $className, string $methodName): ?string + { + $methods = get_class_methods($className); + + $similarMethods = array_filter($methods, function ($method) use ($methodName) { + return levenshtein($methodName, $method) <= 3; + }); + + return $similarMethods ? reset($similarMethods) : null; + } } diff --git a/src/Doctrine/Common/State/LinksHandlerTrait.php b/src/Doctrine/Common/State/LinksHandlerTrait.php index a550ee11ad1..5ff78a3eacf 100644 --- a/src/Doctrine/Common/State/LinksHandlerTrait.php +++ b/src/Doctrine/Common/State/LinksHandlerTrait.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Doctrine\Common\State; -use ApiPlatform\Exception\OperationNotFoundException; +use ApiPlatform\Metadata\Exception\OperationNotFoundException; use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation; use ApiPlatform\Metadata\GraphQl\Query; diff --git a/src/Doctrine/Common/State/PersistProcessor.php b/src/Doctrine/Common/State/PersistProcessor.php index 20e93ac4399..19e575c822c 100644 --- a/src/Doctrine/Common/State/PersistProcessor.php +++ b/src/Doctrine/Common/State/PersistProcessor.php @@ -48,7 +48,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables = // PUT: reset the existing object managed by Doctrine and merge data sent by the user in it // This custom logic is needed because EntityManager::merge() has been deprecated and UPSERT isn't supported: // https://github.com/doctrine/orm/issues/8461#issuecomment-1250233555 - if ($operation instanceof HttpOperation && 'PUT' === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? false)) { + if ($operation instanceof HttpOperation && 'PUT' === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? true)) { \assert(method_exists($manager, 'getReference')); $newData = $data; $identifiers = array_reverse($uriVariables); @@ -110,7 +110,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables = /** * Checks if doctrine does not manage data automatically. */ - private function isDeferredExplicit(DoctrineObjectManager $manager, $data): bool + private function isDeferredExplicit(DoctrineObjectManager $manager, object $data): bool { $classMetadata = $manager->getClassMetadata($this->getObjectClass($data)); if ($classMetadata && method_exists($classMetadata, 'isChangeTrackingDeferredExplicit')) { // @phpstan-ignore-line metadata can be null diff --git a/src/Doctrine/Common/State/RemoveProcessor.php b/src/Doctrine/Common/State/RemoveProcessor.php index cf9b472892c..80f8f38a725 100644 --- a/src/Doctrine/Common/State/RemoveProcessor.php +++ b/src/Doctrine/Common/State/RemoveProcessor.php @@ -40,7 +40,7 @@ public function process(mixed $data, Operation $operation, array $uriVariables = /** * Gets the Doctrine object manager associated with given data. */ - private function getManager($data): ?DoctrineObjectManager + private function getManager(mixed $data): ?DoctrineObjectManager { return \is_object($data) ? $this->managerRegistry->getManagerForClass($this->getObjectClass($data)) : null; } diff --git a/src/Doctrine/Common/Tests/CollectionPaginatorTest.php b/src/Doctrine/Common/Tests/CollectionPaginatorTest.php index 474dd6b4c5b..415ee02c536 100644 --- a/src/Doctrine/Common/Tests/CollectionPaginatorTest.php +++ b/src/Doctrine/Common/Tests/CollectionPaginatorTest.php @@ -19,9 +19,7 @@ class CollectionPaginatorTest extends TestCase { - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize($results, $currentPage, $itemsPerPage, $totalItems, $lastPage, $currentItems): void { $results = new ArrayCollection($results); diff --git a/src/Doctrine/Common/Tests/SelectablePaginatorTest.php b/src/Doctrine/Common/Tests/SelectablePaginatorTest.php index 9e1d37adc7c..7f9c8a37b3d 100644 --- a/src/Doctrine/Common/Tests/SelectablePaginatorTest.php +++ b/src/Doctrine/Common/Tests/SelectablePaginatorTest.php @@ -19,9 +19,7 @@ class SelectablePaginatorTest extends TestCase { - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize($results, $currentPage, $itemsPerPage, $totalItems, $lastPage, $currentItems): void { $results = new ArrayCollection($results); diff --git a/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php b/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php index a71cb3ddb72..b16a9e99f81 100644 --- a/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php +++ b/src/Doctrine/Common/Tests/SelectablePartialPaginatorTest.php @@ -19,9 +19,7 @@ class SelectablePartialPaginatorTest extends TestCase { - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize($results, $currentPage, $itemsPerPage, $currentItems): void { $results = new ArrayCollection($results); diff --git a/src/Doctrine/Common/Tests/State/PersistProcessorTest.php b/src/Doctrine/Common/Tests/State/PersistProcessorTest.php index 1793614040a..3d96380f8df 100644 --- a/src/Doctrine/Common/Tests/State/PersistProcessorTest.php +++ b/src/Doctrine/Common/Tests/State/PersistProcessorTest.php @@ -61,7 +61,7 @@ public function testPersistIfEntityAlreadyManaged(): void $objectManagerProphecy->persist($dummy)->shouldNotBeCalled(); $objectManagerProphecy->flush()->shouldBeCalled(); $objectManagerProphecy->refresh($dummy)->shouldBeCalled(); - $objectManagerProphecy->getClassMetadata(Dummy::class)->willReturn(null)->shouldBeCalled(); + $objectManagerProphecy->getClassMetadata(Dummy::class)->willReturn(new ClassMetadata(Dummy::class))->shouldBeCalled(); $managerRegistryProphecy = $this->prophesize(ManagerRegistry::class); $managerRegistryProphecy->getManagerForClass(Dummy::class)->willReturn($objectManagerProphecy->reveal())->shouldBeCalled(); @@ -91,9 +91,7 @@ public static function getTrackingPolicyParameters(): array ]; } - /** - * @dataProvider getTrackingPolicyParameters - */ + #[\PHPUnit\Framework\Attributes\DataProvider('getTrackingPolicyParameters')] public function testTrackingPolicy(string $metadataClass, bool $deferredExplicit, bool $persisted): void { $dummy = new Dummy(); diff --git a/src/Doctrine/Common/composer.json b/src/Doctrine/Common/composer.json index a2bcc7f5908..87125829d16 100644 --- a/src/Doctrine/Common/composer.json +++ b/src/Doctrine/Common/composer.json @@ -3,10 +3,11 @@ "description": "Common files used by api-platform/doctrine-orm and api-platform/doctrine-odm", "type": "library", "keywords": [ - "DOCTRINE", - "ORM", - "ODM", - "COMMON" + "doctrine", + "orm", + "odm", + "REST", + "GraphQL" ], "homepage": "https://api-platform.com/", "license": "MIT", @@ -22,19 +23,19 @@ } ], "require": { - "php": ">=8.1", - "api-platform/metadata": "@dev", - "api-platform/state": "@dev", + "php": ">=8.2", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "doctrine/collections": "^2.1", "doctrine/common": "^3.2.2", - "doctrine/persistence": "^3.2" + "doctrine/persistence": "^3.2 || ^4.0" }, "require-dev": { - "doctrine/mongodb-odm": "^2.6", + "doctrine/mongodb-odm": "^2.10", "doctrine/orm": "^2.17 || ^3.0", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^10.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0" + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "11.5.x-dev", + "symfony/type-info": "^7.3" }, "conflict": { "doctrine/persistence": "<1.3" @@ -42,7 +43,10 @@ "suggest": { "phpstan/phpdoc-parser": "For PHP documentation support.", "symfony/yaml": "For YAML resource configuration.", - "symfony/config": "For XML resource configuration." + "symfony/config": "For XML resource configuration.", + "api-platform/http-cache": "For HTTP cache invalidation.", + "api-platform/graphql": "For GraphQl mercure subscriptions.", + "symfony/mercure-bundle": "For mercure updates publisher." }, "autoload": { "psr-4": { @@ -57,13 +61,26 @@ }, "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" }, "symfony": { - "require": "^6.4" + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" } }, "scripts": { "test": "./vendor/bin/phpunit" - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] } diff --git a/src/Doctrine/Common/phpunit.xml.dist b/src/Doctrine/Common/phpunit.xml.dist index 968fa633da6..8872d132b00 100644 --- a/src/Doctrine/Common/phpunit.xml.dist +++ b/src/Doctrine/Common/phpunit.xml.dist @@ -9,6 +9,11 @@ + + trigger_deprecation + Doctrine\Deprecations\Deprecation::trigger + Doctrine\Deprecations\Deprecation::delegateTriggerToBackend + ./ diff --git a/src/Doctrine/EventListener/PurgeHttpCacheListener.php b/src/Doctrine/EventListener/PurgeHttpCacheListener.php deleted file mode 100644 index fa7b081e4dc..00000000000 --- a/src/Doctrine/EventListener/PurgeHttpCacheListener.php +++ /dev/null @@ -1,182 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Doctrine\EventListener; - -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Exception\OperationNotFoundException; -use ApiPlatform\Exception\RuntimeException; -use ApiPlatform\HttpCache\PurgerInterface; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Event\OnFlushEventArgs; -use Doctrine\ORM\Event\PreUpdateEventArgs; -use Doctrine\ORM\Mapping\AssociationMapping; -use Doctrine\ORM\PersistentCollection; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; - -/** - * Purges responses containing modified entities from the proxy cache. - * - * @author Kévin Dunglas - */ -final class PurgeHttpCacheListener -{ - use ClassInfoTrait; - private readonly PropertyAccessorInterface $propertyAccessor; - private array $tags = []; - - public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null) - { - $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); - } - - /** - * Collects tags from the previous and the current version of the updated entities to purge related documents. - */ - public function preUpdate(PreUpdateEventArgs $eventArgs): void - { - $object = $eventArgs->getObject(); - $this->gatherResourceAndItemTags($object, true); - - $changeSet = $eventArgs->getEntityChangeSet(); - // @phpstan-ignore-next-line - $objectManager = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); - $associationMappings = $objectManager->getClassMetadata(\get_class($eventArgs->getObject()))->getAssociationMappings(); - - foreach ($changeSet as $key => $value) { - if (!isset($associationMappings[$key])) { - continue; - } - - $this->addTagsFor($value[0]); - $this->addTagsFor($value[1]); - } - } - - /** - * Collects tags from inserted and deleted entities, including relations. - */ - public function onFlush(OnFlushEventArgs $eventArgs): void - { - // @phpstan-ignore-next-line - $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); - $uow = $em->getUnitOfWork(); - - foreach ($uow->getScheduledEntityInsertions() as $entity) { - $this->gatherResourceAndItemTags($entity, false); - $this->gatherRelationTags($em, $entity); - } - - foreach ($uow->getScheduledEntityUpdates() as $entity) { - $this->gatherResourceAndItemTags($entity, true); - $this->gatherRelationTags($em, $entity); - } - - foreach ($uow->getScheduledEntityDeletions() as $entity) { - $this->gatherResourceAndItemTags($entity, true); - $this->gatherRelationTags($em, $entity); - } - } - - /** - * Purges tags collected during this request, and clears the tag list. - */ - public function postFlush(): void - { - if (empty($this->tags)) { - return; - } - - $this->purger->purge(array_values($this->tags)); - - $this->tags = []; - } - - private function gatherResourceAndItemTags(object $entity, bool $purgeItem): void - { - try { - $resourceClass = $this->resourceClassResolver->getResourceClass($entity); - $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, new GetCollection()); - $this->tags[$iri] = $iri; - - if ($purgeItem) { - $this->addTagForItem($entity); - } - } catch (OperationNotFoundException|InvalidArgumentException) { - } - } - - private function gatherRelationTags(EntityManagerInterface $em, object $entity): void - { - $associationMappings = $em->getClassMetadata($entity::class)->getAssociationMappings(); - /** @var array|AssociationMapping $associationMapping according to the version of doctrine orm */ - foreach ($associationMappings as $property => $associationMapping) { - if ($associationMapping instanceof AssociationMapping && ($associationMapping->targetEntity ?? null) && !$this->resourceClassResolver->isResourceClass($associationMapping->targetEntity)) { - return; - } - - if ( - \is_array($associationMapping) - && \array_key_exists('targetEntity', $associationMapping) - && !$this->resourceClassResolver->isResourceClass($associationMapping['targetEntity'])) { - return; - } - - if ($this->propertyAccessor->isReadable($entity, $property)) { - $this->addTagsFor($this->propertyAccessor->getValue($entity, $property)); - } - } - } - - private function addTagsFor(mixed $value): void - { - if (!$value || \is_scalar($value)) { - return; - } - - if (!is_iterable($value)) { - $this->addTagForItem($value); - - return; - } - - if ($value instanceof PersistentCollection) { - $value = clone $value; - } - - foreach ($value as $v) { - $this->addTagForItem($v); - } - } - - private function addTagForItem(mixed $value): void - { - if (!$this->resourceClassResolver->isResourceClass($this->getObjectClass($value))) { - return; - } - - try { - $iri = $this->iriConverter->getIriFromResource($value); - $this->tags[$iri] = $iri; - } catch (RuntimeException|InvalidArgumentException) { - } - } -} diff --git a/src/Doctrine/Odm/.gitattributes b/src/Doctrine/Odm/.gitattributes index ae3c2e1685a..801f2080d71 100644 --- a/src/Doctrine/Odm/.gitattributes +++ b/src/Doctrine/Odm/.gitattributes @@ -1,2 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore /.gitignore export-ignore /Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/Doctrine/Odm/.github/workflows/close_pr.yml b/src/Doctrine/Odm/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/Doctrine/Odm/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/Doctrine/Odm/AbstractPaginator.php b/src/Doctrine/Odm/AbstractPaginator.php new file mode 100644 index 00000000000..d30f1537aba --- /dev/null +++ b/src/Doctrine/Odm/AbstractPaginator.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Odm; + +use ApiPlatform\State\Pagination\PartialPaginatorInterface; + +abstract class AbstractPaginator implements \IteratorAggregate, PartialPaginatorInterface +{ + protected int $firstResult; + protected int $maxResults; + protected \ArrayIterator $iterator; + protected int $count; + + public function __construct(array $result) + { + $this->firstResult = $result['__api_first_result__']; + $this->maxResults = $result['__api_max_results__']; + } + + /** + * {@inheritdoc} + */ + public function getCurrentPage(): float + { + if (0 >= $this->maxResults) { + return 1.; + } + + return floor($this->firstResult / $this->maxResults) + 1.; + } + + /** + * {@inheritdoc} + */ + public function getItemsPerPage(): float + { + return (float) $this->maxResults; + } + + /** + * {@inheritdoc} + */ + public function getIterator(): \Traversable + { + return $this->iterator; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + return $this->count; + } +} diff --git a/src/Doctrine/Odm/Extension/PaginationExtension.php b/src/Doctrine/Odm/Extension/PaginationExtension.php index f1c7892f5ca..c1d832ab32c 100644 --- a/src/Doctrine/Odm/Extension/PaginationExtension.php +++ b/src/Doctrine/Odm/Extension/PaginationExtension.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Doctrine\Odm\Extension; use ApiPlatform\Doctrine\Odm\Paginator; +use ApiPlatform\Doctrine\Odm\PartialPaginator; use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\Pagination\Pagination; @@ -50,7 +51,9 @@ public function applyToCollection(Builder $aggregationBuilder, string $resourceC return; } - $context = $this->addCountToContext(clone $aggregationBuilder, $context); + if ($doesCount = !$this->pagination->isPartialEnabled($operation, $context)) { + $context = $this->addCountToContext(clone $aggregationBuilder, $context); + } [, $offset, $limit] = $this->pagination->getPagination($operation, $context); @@ -63,23 +66,27 @@ public function applyToCollection(Builder $aggregationBuilder, string $resourceC * @var DocumentRepository */ $repository = $manager->getRepository($resourceClass); - $resultsAggregationBuilder = $repository->createAggregationBuilder()->skip($offset); + + $facet = $aggregationBuilder->facet(); + $addFields = $aggregationBuilder->addFields(); + + // Get the results slice, from $offset to $offset + $limit + // MongoDB does not support $limit: O, so we return an empty array directly if ($limit > 0) { - $resultsAggregationBuilder->limit($limit); + $facet->field('results')->pipeline($repository->createAggregationBuilder()->skip($offset)->limit($limit)); } else { - // Results have to be 0 but MongoDB does not support a limit equal to 0. - $resultsAggregationBuilder->match()->field(Paginator::LIMIT_ZERO_MARKER_FIELD)->equals(Paginator::LIMIT_ZERO_MARKER); + $addFields->field('results')->literal([]); + } + + // Count the total number of items + if ($doesCount) { + $facet->field('count')->pipeline($repository->createAggregationBuilder()->count('count')); } - $aggregationBuilder - ->facet() - ->field('results')->pipeline( - $resultsAggregationBuilder - ) - ->field('count')->pipeline( - $repository->createAggregationBuilder() - ->count('count') - ); + // Store pagination metadata, read by the Paginator + // Using __ to avoid field names mapping + $addFields->field('__api_first_result__')->literal($offset); + $addFields->field('__api_max_results__')->literal($limit); } /** @@ -109,7 +116,13 @@ public function getResult(Builder $aggregationBuilder, string $resourceClass, ?O $attribute = $operation?->getExtraProperties()['doctrine_mongodb'] ?? []; $executeOptions = $attribute['execute_options'] ?? []; - return new Paginator($aggregationBuilder->execute($executeOptions), $manager->getUnitOfWork(), $resourceClass, $aggregationBuilder->getPipeline()); + $iterator = $aggregationBuilder->getAggregation($executeOptions)->getIterator(); + + if ($this->pagination->isPartialEnabled($operation, $context)) { + return new PartialPaginator($iterator, $manager->getUnitOfWork(), $resourceClass); + } + + return new Paginator($iterator, $manager->getUnitOfWork(), $resourceClass); } private function addCountToContext(Builder $aggregationBuilder, array $context): array diff --git a/src/Doctrine/Odm/Extension/ParameterExtension.php b/src/Doctrine/Odm/Extension/ParameterExtension.php index 864eae550c1..d68e6e9ed3b 100644 --- a/src/Doctrine/Odm/Extension/ParameterExtension.php +++ b/src/Doctrine/Odm/Extension/ParameterExtension.php @@ -13,12 +13,17 @@ namespace ApiPlatform\Doctrine\Odm\Extension; +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; use ApiPlatform\Doctrine\Common\ParameterValueExtractorTrait; +use ApiPlatform\Doctrine\Odm\Filter\AbstractFilter; use ApiPlatform\Doctrine\Odm\Filter\FilterInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ParameterNotFound; +use Doctrine\Bundle\MongoDBBundle\ManagerRegistry; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; /** * Reads operation parameters and execute its filter. @@ -29,14 +34,20 @@ final class ParameterExtension implements AggregationCollectionExtensionInterfac { use ParameterValueExtractorTrait; - public function __construct(private readonly ContainerInterface $filterLocator) - { + public function __construct( + private readonly ContainerInterface $filterLocator, + private readonly ?ManagerRegistry $managerRegistry = null, + private readonly ?LoggerInterface $logger = null, + ) { } + /** + * @param array $context + */ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass = null, ?Operation $operation = null, array &$context = []): void { foreach ($operation->getParameters() ?? [] as $parameter) { - if (!($v = $parameter->getValue()) || $v instanceof ParameterNotFound) { + if (null === ($v = $parameter->getValue()) || $v instanceof ParameterNotFound) { continue; } @@ -45,15 +56,49 @@ private function applyFilter(Builder $aggregationBuilder, ?string $resourceClass continue; } - $filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null; - if ($filter instanceof FilterInterface) { - $filterContext = ['filters' => $values]; - $filter->apply($aggregationBuilder, $resourceClass, $operation, $filterContext); - // update by reference - if (isset($filterContext['mongodb_odm_sort_fields'])) { - $context['mongodb_odm_sort_fields'] = $filterContext['mongodb_odm_sort_fields']; + $filter = match (true) { + $filterId instanceof FilterInterface => $filterId, + \is_string($filterId) && $this->filterLocator->has($filterId) => $this->filterLocator->get($filterId), + default => null, + }; + + if (!$filter instanceof FilterInterface) { + continue; + } + + if ($this->managerRegistry && $filter instanceof ManagerRegistryAwareInterface && !$filter->hasManagerRegistry()) { + $filter->setManagerRegistry($this->managerRegistry); + } + + if ($this->logger && $filter instanceof LoggerAwareInterface && !$filter->hasLogger()) { + $filter->setLogger($this->logger); + } + + if ($filter instanceof AbstractFilter && !$filter->getProperties()) { + $propertyKey = $parameter->getProperty() ?? $parameter->getKey(); + + if (str_contains($propertyKey, ':property')) { + $extraProperties = $parameter->getExtraProperties()['_properties'] ?? []; + foreach (array_keys($extraProperties) as $property) { + $properties[$property] = $parameter->getFilterContext(); + } + } else { + $properties = [$propertyKey => $parameter->getFilterContext()]; } + + $filter->setProperties($properties ?? []); } + + $context['filters'] = $values; + $context['parameter'] = $parameter; + + $filter->apply($aggregationBuilder, $resourceClass, $operation, $context); + + unset($context['filters'], $context['parameter']); + } + + if (isset($context['match'])) { + $aggregationBuilder->match()->addAnd($context['match']); } } diff --git a/src/Doctrine/Odm/Filter/AbstractFilter.php b/src/Doctrine/Odm/Filter/AbstractFilter.php index 87c30390c32..fd5ea562c63 100644 --- a/src/Doctrine/Odm/Filter/AbstractFilter.php +++ b/src/Doctrine/Odm/Filter/AbstractFilter.php @@ -13,9 +13,11 @@ namespace ApiPlatform\Doctrine\Odm\Filter; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface; use ApiPlatform\Doctrine\Common\PropertyHelperTrait; use ApiPlatform\Doctrine\Odm\PropertyHelperTrait as MongoDbOdmPropertyHelperTrait; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Operation; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\Persistence\ManagerRegistry; @@ -30,14 +32,18 @@ * * @author Alan Poulain */ -abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface +abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface, ManagerRegistryAwareInterface { use MongoDbOdmPropertyHelperTrait; use PropertyHelperTrait; protected LoggerInterface $logger; - public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null) - { + public function __construct( + protected ?ManagerRegistry $managerRegistry = null, + ?LoggerInterface $logger = null, + protected ?array $properties = null, + protected ?NameConverterInterface $nameConverter = null, + ) { $this->logger = $logger ?? new NullLogger(); } @@ -54,20 +60,37 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Opera /** * Passes a property through the filter. */ - abstract protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void; + abstract protected function filterProperty(string $property, mixed $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void; + + public function hasManagerRegistry(): bool + { + return $this->managerRegistry instanceof ManagerRegistry; + } - protected function getManagerRegistry(): ManagerRegistry + public function getManagerRegistry(): ManagerRegistry { + if (!$this->hasManagerRegistry()) { + throw new RuntimeException('ManagerRegistry must be initialized before accessing it.'); + } + return $this->managerRegistry; } - protected function getProperties(): ?array + public function setManagerRegistry(ManagerRegistry $managerRegistry): void + { + $this->managerRegistry = $managerRegistry; + } + + /** + * @return array|null + */ + public function getProperties(): ?array { return $this->properties; } /** - * @param string[] $properties + * @param array $properties */ public function setProperties(array $properties): void { diff --git a/src/Doctrine/Odm/Filter/BooleanFilter.php b/src/Doctrine/Odm/Filter/BooleanFilter.php index babe3309ed0..855b593e0ea 100644 --- a/src/Doctrine/Odm/Filter/BooleanFilter.php +++ b/src/Doctrine/Odm/Filter/BooleanFilter.php @@ -14,7 +14,9 @@ namespace ApiPlatform\Doctrine\Odm\Filter; use ApiPlatform\Doctrine\Common\Filter\BooleanFilterTrait; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Types\Type as MongoDbType; @@ -104,7 +106,7 @@ * @author Teoh Han Hui * @author Alan Poulain */ -final class BooleanFilter extends AbstractFilter +final class BooleanFilter extends AbstractFilter implements JsonSchemaFilterInterface { use BooleanFilterTrait; @@ -116,7 +118,7 @@ final class BooleanFilter extends AbstractFilter /** * {@inheritdoc} */ - protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + protected function filterProperty(string $property, mixed $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { if ( !$this->isPropertyEnabled($property, $resourceClass) @@ -139,4 +141,12 @@ protected function filterProperty(string $property, $value, Builder $aggregation $aggregationBuilder->match()->field($matchField)->equals($value); } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'boolean']; + } } diff --git a/src/Doctrine/Odm/Filter/DateFilter.php b/src/Doctrine/Odm/Filter/DateFilter.php index 8be5534fbca..227888c80f2 100644 --- a/src/Doctrine/Odm/Filter/DateFilter.php +++ b/src/Doctrine/Odm/Filter/DateFilter.php @@ -16,7 +16,12 @@ use ApiPlatform\Doctrine\Common\Filter\DateFilterInterface; use ApiPlatform\Doctrine\Common\Filter\DateFilterTrait; use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Types\Type as MongoDbType; @@ -117,7 +122,7 @@ * @author Théo FIDRY * @author Alan Poulain */ -final class DateFilter extends AbstractFilter implements DateFilterInterface +final class DateFilter extends AbstractFilter implements DateFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface { use DateFilterTrait; @@ -129,11 +134,11 @@ final class DateFilter extends AbstractFilter implements DateFilterInterface /** * {@inheritdoc} */ - protected function filterProperty(string $property, $values, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + protected function filterProperty(string $property, mixed $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { - // Expect $values to be an array having the period as keys and the date value as values + // Expect $value to be an array having the period as keys and the date value as values if ( - !\is_array($values) + !\is_array($value) || !$this->isPropertyEnabled($property, $resourceClass) || !$this->isPropertyMapped($property, $resourceClass) || !$this->isDateField($property, $resourceClass) @@ -153,42 +158,42 @@ protected function filterProperty(string $property, $values, Builder $aggregatio $aggregationBuilder->match()->field($matchField)->notEqual(null); } - if (isset($values[self::PARAMETER_BEFORE])) { + if (isset($value[self::PARAMETER_BEFORE])) { $this->addMatch( $aggregationBuilder, $matchField, self::PARAMETER_BEFORE, - $values[self::PARAMETER_BEFORE], + $value[self::PARAMETER_BEFORE], $nullManagement ); } - if (isset($values[self::PARAMETER_STRICTLY_BEFORE])) { + if (isset($value[self::PARAMETER_STRICTLY_BEFORE])) { $this->addMatch( $aggregationBuilder, $matchField, self::PARAMETER_STRICTLY_BEFORE, - $values[self::PARAMETER_STRICTLY_BEFORE], + $value[self::PARAMETER_STRICTLY_BEFORE], $nullManagement ); } - if (isset($values[self::PARAMETER_AFTER])) { + if (isset($value[self::PARAMETER_AFTER])) { $this->addMatch( $aggregationBuilder, $matchField, self::PARAMETER_AFTER, - $values[self::PARAMETER_AFTER], + $value[self::PARAMETER_AFTER], $nullManagement ); } - if (isset($values[self::PARAMETER_STRICTLY_AFTER])) { + if (isset($value[self::PARAMETER_STRICTLY_AFTER])) { $this->addMatch( $aggregationBuilder, $matchField, self::PARAMETER_STRICTLY_AFTER, - $values[self::PARAMETER_STRICTLY_AFTER], + $value[self::PARAMETER_STRICTLY_AFTER], $nullManagement ); } @@ -197,7 +202,7 @@ protected function filterProperty(string $property, $values, Builder $aggregatio /** * Adds the match stage according to the chosen null management. */ - private function addMatch(Builder $aggregationBuilder, string $field, string $operator, $value, ?string $nullManagement = null): void + private function addMatch(Builder $aggregationBuilder, string $field, string $operator, mixed $value, ?string $nullManagement = null): void { $value = $this->normalizeValue($value, $operator); @@ -237,4 +242,25 @@ private function addMatch(Builder $aggregationBuilder, string $field, string $op $aggregationBuilder->match()->addAnd($aggregationBuilder->matchExpr()->field($field)->operator($operatorValue[$operator], $value)); } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'string', 'format' => 'date']; + } + + public function getOpenApiParameters(Parameter $parameter): array + { + $in = $parameter instanceof QueryParameter ? 'query' : 'header'; + $key = $parameter->getKey(); + + return [ + new OpenApiParameter(name: $key.'[after]', in: $in), + new OpenApiParameter(name: $key.'[before]', in: $in), + new OpenApiParameter(name: $key.'[strictly_after]', in: $in), + new OpenApiParameter(name: $key.'[strictly_before]', in: $in), + ]; + } } diff --git a/src/Doctrine/Odm/Filter/ExactFilter.php b/src/Doctrine/Odm/Filter/ExactFilter.php new file mode 100644 index 00000000000..ccb883d86e6 --- /dev/null +++ b/src/Doctrine/Odm/Filter/ExactFilter.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Odm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ODM\MongoDB\Aggregation\Builder; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\LockException; +use Doctrine\ODM\MongoDB\Mapping\MappingException; + +/** + * @author Vincent Amstoutz + */ +final class ExactFilter implements FilterInterface, OpenApiParameterFilterInterface, ManagerRegistryAwareInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use ManagerRegistryAwareTrait; + use OpenApiFilterTrait; + + /** + * @throws MappingException + * @throws LockException + */ + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + $parameter = $context['parameter']; + $property = $parameter->getProperty(); + $value = $parameter->getValue(); + $operator = $context['operator'] ?? 'addAnd'; + $match = $context['match'] = $context['match'] ?? + $aggregationBuilder + ->matchExpr(); + + $documentManager = $this->getManagerRegistry()->getManagerForClass($resourceClass); + if (!$documentManager instanceof DocumentManager) { + return; + } + + $classMetadata = $documentManager->getClassMetadata($resourceClass); + + if (!$classMetadata->hasReference($property)) { + $match + ->{$operator}($aggregationBuilder->matchExpr()->field($property)->{is_iterable($value) ? 'in' : 'equals'}($value)); + + return; + } + + $mapping = $classMetadata->getFieldMapping($property); + $method = $classMetadata->isSingleValuedAssociation($property) ? 'references' : 'includesReferenceTo'; + + if (is_iterable($value)) { + $or = $aggregationBuilder->matchExpr(); + + foreach ($value as $v) { + $or->addOr($aggregationBuilder->matchExpr()->field($property)->{$method}($documentManager->getPartialReference($mapping['targetDocument'], $v))); + } + + $match->{$operator}($or); + + return; + } + + $match + ->{$operator}( + $aggregationBuilder->matchExpr() + ->field($property) + ->{$method}($documentManager->getPartialReference($mapping['targetDocument'], $value)) + ); + } +} diff --git a/src/Doctrine/Odm/Filter/ExistsFilter.php b/src/Doctrine/Odm/Filter/ExistsFilter.php index db57f81fdad..a4f751b3a9e 100644 --- a/src/Doctrine/Odm/Filter/ExistsFilter.php +++ b/src/Doctrine/Odm/Filter/ExistsFilter.php @@ -15,7 +15,11 @@ use ApiPlatform\Doctrine\Common\Filter\ExistsFilterInterface; use ApiPlatform\Doctrine\Common\Filter\ExistsFilterTrait; +use ApiPlatform\Doctrine\Common\Filter\PropertyPlaceholderOpenApiParameterTrait; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; @@ -107,11 +111,12 @@ * @author Teoh Han Hui * @author Alan Poulain */ -final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface +final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface { use ExistsFilterTrait; + use PropertyPlaceholderOpenApiParameterTrait; - public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null) + public function __construct(?ManagerRegistry $managerRegistry = null, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); @@ -123,6 +128,13 @@ public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $ */ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { + $parameter = $context['parameter'] ?? null; + if (null !== ($value = $context['filters'][$parameter?->getProperty()] ?? null)) { + $this->filterProperty($this->denormalizePropertyName($parameter->getProperty()), $value, $aggregationBuilder, $resourceClass, $operation, $context); + + return; + } + foreach ($context['filters'][$this->existsParameterName] ?? [] as $property => $value) { $this->filterProperty($this->denormalizePropertyName($property), $value, $aggregationBuilder, $resourceClass, $operation, $context); } @@ -131,7 +143,7 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Opera /** * {@inheritdoc} */ - protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + protected function filterProperty(string $property, mixed $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { if ( !$this->isPropertyEnabled($property, $resourceClass) @@ -167,4 +179,9 @@ protected function isNullableField(string $property, string $resourceClass): boo return $metadata instanceof ClassMetadata && $metadata->hasField($field) ? $metadata->isNullable($field) : false; } + + public function getSchema(Parameter $parameter): array + { + return ['type' => 'boolean']; + } } diff --git a/src/Doctrine/Odm/Filter/FilterInterface.php b/src/Doctrine/Odm/Filter/FilterInterface.php index 11e006abb8f..4395a25df55 100644 --- a/src/Doctrine/Odm/Filter/FilterInterface.php +++ b/src/Doctrine/Odm/Filter/FilterInterface.php @@ -15,6 +15,7 @@ use ApiPlatform\Metadata\FilterInterface as BaseFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\ODM\MongoDB\Aggregation\Builder; /** @@ -26,6 +27,8 @@ interface FilterInterface extends BaseFilterInterface { /** * Applies the filter. + * + * @param array|array{filters?: array|array, parameter?: Parameter, mongodb_odm_sort_fields?: array, ...} $context */ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void; } diff --git a/src/Doctrine/Odm/Filter/FreeTextQueryFilter.php b/src/Doctrine/Odm/Filter/FreeTextQueryFilter.php new file mode 100644 index 00000000000..2dde16d6ecc --- /dev/null +++ b/src/Doctrine/Odm/Filter/FreeTextQueryFilter.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Odm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\Operation; +use Doctrine\ODM\MongoDB\Aggregation\Builder; + +final class FreeTextQueryFilter implements FilterInterface, ManagerRegistryAwareInterface, LoggerAwareInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use LoggerAwareTrait; + use ManagerRegistryAwareTrait; + + /** + * @param list $properties an array of properties, defaults to `parameter->getProperties()` + */ + public function __construct(private readonly FilterInterface $filter, private readonly ?array $properties = null) + { + } + + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + if ($this->filter instanceof ManagerRegistryAwareInterface) { + $this->filter->setManagerRegistry($this->getManagerRegistry()); + } + + if ($this->filter instanceof LoggerAwareInterface) { + $this->filter->setLogger($this->getLogger()); + } + + $parameter = $context['parameter']; + foreach ($this->properties ?? $parameter->getProperties() ?? [] as $property) { + $newContext = ['parameter' => $parameter->withProperty($property), 'match' => $context['match'] ?? $aggregationBuilder->match()->expr()] + $context; + $this->filter->apply( + $aggregationBuilder, + $resourceClass, + $operation, + $newContext, + ); + + if (isset($newContext['match'])) { + $context['match'] = $newContext['match']; + } + } + } +} diff --git a/src/Doctrine/Odm/Filter/IriFilter.php b/src/Doctrine/Odm/Filter/IriFilter.php new file mode 100644 index 00000000000..4f0d742dc1b --- /dev/null +++ b/src/Doctrine/Odm/Filter/IriFilter.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Odm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\ParameterProviderFilterInterface; +use ApiPlatform\State\ParameterProvider\IriConverterParameterProvider; +use Doctrine\ODM\MongoDB\Aggregation\Builder; +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Mapping\MappingException; + +/** + * @author Vincent Amstoutz + */ +final class IriFilter implements FilterInterface, OpenApiParameterFilterInterface, ParameterProviderFilterInterface, ManagerRegistryAwareInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use ManagerRegistryAwareTrait; + use OpenApiFilterTrait; + + /** + * @throws MappingException + */ + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + $parameter = $context['parameter']; + $value = $parameter->getValue(); + $operator = $context['operator'] ?? 'addAnd'; + $match = $context['match'] = $context['match'] ?? + $aggregationBuilder + ->matchExpr(); + + $documentManager = $this->getManagerRegistry()->getManagerForClass($resourceClass); + if (!$documentManager instanceof DocumentManager) { + return; + } + + $classMetadata = $documentManager->getClassMetadata($resourceClass); + $property = $parameter->getProperty(); + if (!$classMetadata->hasReference($property)) { + return; + } + + $method = $classMetadata->isSingleValuedAssociation($property) ? 'references' : 'includesReferenceTo'; + + if (is_iterable($value)) { + $or = $aggregationBuilder->matchExpr(); + + foreach ($value as $v) { + $or->addOr($aggregationBuilder->matchExpr()->field($property)->{$method}($v)); + } + + $match->{$operator}($or); + + return; + } + + $match + ->{$operator}( + $aggregationBuilder + ->matchExpr() + ->field($property) + ->{$method}($value) + ); + } + + public static function getParameterProvider(): string + { + return IriConverterParameterProvider::class; + } +} diff --git a/src/Doctrine/Odm/Filter/NumericFilter.php b/src/Doctrine/Odm/Filter/NumericFilter.php index 34cec17bb05..c6122e4705e 100644 --- a/src/Doctrine/Odm/Filter/NumericFilter.php +++ b/src/Doctrine/Odm/Filter/NumericFilter.php @@ -14,7 +14,9 @@ namespace ApiPlatform\Doctrine\Odm\Filter; use ApiPlatform\Doctrine\Common\Filter\NumericFilterTrait; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Types\Type as MongoDbType; @@ -104,7 +106,7 @@ * @author Teoh Han Hui * @author Alan Poulain */ -final class NumericFilter extends AbstractFilter +final class NumericFilter extends AbstractFilter implements JsonSchemaFilterInterface { use NumericFilterTrait; @@ -120,7 +122,7 @@ final class NumericFilter extends AbstractFilter /** * {@inheritdoc} */ - protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + protected function filterProperty(string $property, mixed $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { if ( !$this->isPropertyEnabled($property, $resourceClass) @@ -163,4 +165,9 @@ protected function getType(?string $doctrineType = null): string return 'int'; } + + public function getSchema(Parameter $parameter): array + { + return ['type' => 'number']; + } } diff --git a/src/Doctrine/Odm/Filter/OrFilter.php b/src/Doctrine/Odm/Filter/OrFilter.php new file mode 100644 index 00000000000..9017bceba13 --- /dev/null +++ b/src/Doctrine/Odm/Filter/OrFilter.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Odm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ODM\MongoDB\Aggregation\Builder; + +/** + * @author Vincent Amstoutz + */ +final class OrFilter implements FilterInterface, OpenApiParameterFilterInterface, ManagerRegistryAwareInterface, LoggerAwareInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use LoggerAwareTrait; + use ManagerRegistryAwareTrait; + use OpenApiFilterTrait; + + public function __construct(private readonly FilterInterface $filter) + { + } + + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + if ($this->filter instanceof ManagerRegistryAwareInterface) { + $this->filter->setManagerRegistry($this->getManagerRegistry()); + } + + if ($this->filter instanceof LoggerAwareInterface) { + $this->filter->setLogger($this->getLogger()); + } + + $newContext = ['operator' => 'addOr'] + $context; + $this->filter->apply($aggregationBuilder, $resourceClass, $operation, $newContext); + if (isset($newContext['match'])) { + $context['match'] = $newContext['match']; + } + } +} diff --git a/src/Doctrine/Odm/Filter/OrderFilter.php b/src/Doctrine/Odm/Filter/OrderFilter.php index a8cf49b1212..105c3327c95 100644 --- a/src/Doctrine/Odm/Filter/OrderFilter.php +++ b/src/Doctrine/Odm/Filter/OrderFilter.php @@ -15,7 +15,11 @@ use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface; use ApiPlatform\Doctrine\Common\Filter\OrderFilterTrait; +use ApiPlatform\Doctrine\Common\Filter\PropertyPlaceholderOpenApiParameterTrait; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\Persistence\ManagerRegistry; use Psr\Log\LoggerInterface; @@ -196,11 +200,12 @@ * @author Théo FIDRY * @author Alan Poulain */ -final class OrderFilter extends AbstractFilter implements OrderFilterInterface +final class OrderFilter extends AbstractFilter implements OrderFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface { use OrderFilterTrait; + use PropertyPlaceholderOpenApiParameterTrait; - public function __construct(ManagerRegistry $managerRegistry, string $orderParameterName = 'order', ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null) + public function __construct(?ManagerRegistry $managerRegistry = null, string $orderParameterName = 'order', ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null) { if (null !== $properties) { $properties = array_map(static function ($propertyOptions) { @@ -225,12 +230,17 @@ public function __construct(ManagerRegistry $managerRegistry, string $orderParam */ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { - if (isset($context['filters']) && !isset($context['filters'][$this->orderParameterName])) { + if ( + isset($context['filters']) + && (!isset($context['filters'][$this->orderParameterName]) || !\is_array($context['filters'][$this->orderParameterName])) + && !isset($context['parameter']) + ) { return; } - if (!isset($context['filters'][$this->orderParameterName]) || !\is_array($context['filters'][$this->orderParameterName])) { - parent::apply($aggregationBuilder, $resourceClass, $operation, $context); + $parameter = $context['parameter'] ?? null; + if (null !== ($value = $context['filters'][$parameter?->getProperty()] ?? null)) { + $this->filterProperty($this->denormalizePropertyName($parameter->getProperty()), $value, $aggregationBuilder, $resourceClass, $operation, $context); return; } @@ -243,13 +253,13 @@ public function apply(Builder $aggregationBuilder, string $resourceClass, ?Opera /** * {@inheritdoc} */ - protected function filterProperty(string $property, $direction, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + protected function filterProperty(string $property, mixed $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { if (!$this->isPropertyEnabled($property, $resourceClass) || !$this->isPropertyMapped($property, $resourceClass)) { return; } - $direction = $this->normalizeValue($direction, $property); + $direction = $this->normalizeValue($value, $property); if (null === $direction) { return; } @@ -264,4 +274,12 @@ protected function filterProperty(string $property, $direction, Builder $aggrega $context['mongodb_odm_sort_fields'] = ($context['mongodb_odm_sort_fields'] ?? []) + [$matchField => $direction] ); } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'string', 'enum' => ['asc', 'desc']]; + } } diff --git a/src/Doctrine/Odm/Filter/PartialSearchFilter.php b/src/Doctrine/Odm/Filter/PartialSearchFilter.php new file mode 100644 index 00000000000..f5fd2f1bb32 --- /dev/null +++ b/src/Doctrine/Odm/Filter/PartialSearchFilter.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Odm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ODM\MongoDB\Aggregation\Builder; +use MongoDB\BSON\Regex; + +/** + * @author Vincent Amstoutz + */ +final class PartialSearchFilter implements FilterInterface, OpenApiParameterFilterInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use OpenApiFilterTrait; + + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + $parameter = $context['parameter']; + $property = $parameter->getProperty(); + $values = $parameter->getValue(); + $match = $context['match'] = $context['match'] ?? + $aggregationBuilder + ->matchExpr(); + $operator = $context['operator'] ?? 'addAnd'; + + if (!is_iterable($values)) { + $escapedValue = preg_quote($values, '/'); + $match->{$operator}( + $aggregationBuilder->matchExpr()->field($property)->equals(new Regex($escapedValue, 'i')) + ); + + return; + } + + $or = $aggregationBuilder->matchExpr(); + foreach ($values as $value) { + $escapedValue = preg_quote($value, '/'); + + $or->addOr( + $aggregationBuilder->matchExpr() + ->field($property) + ->equals(new Regex($escapedValue, 'i')) + ); + } + + $match->{$operator}($or); + } +} diff --git a/src/Doctrine/Odm/Filter/RangeFilter.php b/src/Doctrine/Odm/Filter/RangeFilter.php index e34733df07d..0356b29b6fa 100644 --- a/src/Doctrine/Odm/Filter/RangeFilter.php +++ b/src/Doctrine/Odm/Filter/RangeFilter.php @@ -15,7 +15,11 @@ use ApiPlatform\Doctrine\Common\Filter\RangeFilterInterface; use ApiPlatform\Doctrine\Common\Filter\RangeFilterTrait; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use Doctrine\ODM\MongoDB\Aggregation\Builder; /** @@ -104,14 +108,14 @@ * @author Lee Siong Chan * @author Alan Poulain */ -final class RangeFilter extends AbstractFilter implements RangeFilterInterface +final class RangeFilter extends AbstractFilter implements RangeFilterInterface, OpenApiParameterFilterInterface { use RangeFilterTrait; /** * {@inheritdoc} */ - protected function filterProperty(string $property, $values, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + protected function filterProperty(string $property, mixed $values, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { if ( !\is_array($values) @@ -204,4 +208,17 @@ protected function addMatch(Builder $aggregationBuilder, string $field, string $ break; } } + + public function getOpenApiParameters(Parameter $parameter): array + { + $in = $parameter instanceof QueryParameter ? 'query' : 'header'; + $key = $parameter->getKey(); + + return [ + new OpenApiParameter(name: $key.'[gt]', in: $in), + new OpenApiParameter(name: $key.'[lt]', in: $in), + new OpenApiParameter(name: $key.'[gte]', in: $in), + new OpenApiParameter(name: $key.'[lte]', in: $in), + ]; + } } diff --git a/src/Doctrine/Odm/Filter/SearchFilter.php b/src/Doctrine/Odm/Filter/SearchFilter.php index fdb8c9d271c..8d597836fa3 100644 --- a/src/Doctrine/Odm/Filter/SearchFilter.php +++ b/src/Doctrine/Odm/Filter/SearchFilter.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Doctrine\Odm\Filter; -use ApiPlatform\Api\IdentifiersExtractorInterface as LegacyIdentifiersExtractorInterface; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Doctrine\Common\Filter\SearchFilterInterface; use ApiPlatform\Doctrine\Common\Filter\SearchFilterTrait; use ApiPlatform\Metadata\Exception\InvalidArgumentException; @@ -142,7 +140,7 @@ final class SearchFilter extends AbstractFilter implements SearchFilterInterface public const DOCTRINE_INTEGER_TYPE = [MongoDbType::INTEGER, MongoDbType::INT]; - public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface|LegacyIriConverterInterface $iriConverter, IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface|null $identifiersExtractor, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null) + public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, ?IdentifiersExtractorInterface $identifiersExtractor, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); @@ -151,7 +149,7 @@ public function __construct(ManagerRegistry $managerRegistry, IriConverterInterf $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } - protected function getIriConverter(): LegacyIriConverterInterface|IriConverterInterface + protected function getIriConverter(): IriConverterInterface { return $this->iriConverter; } @@ -164,7 +162,7 @@ protected function getPropertyAccessor(): PropertyAccessorInterface /** * {@inheritdoc} */ - protected function filterProperty(string $property, $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + protected function filterProperty(string $property, mixed $value, Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void { if ( null === $value diff --git a/src/Doctrine/Odm/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactory.php b/src/Doctrine/Odm/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactory.php index 23150df996e..9e07b401b69 100644 --- a/src/Doctrine/Odm/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactory.php +++ b/src/Doctrine/Odm/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactory.php @@ -57,6 +57,10 @@ public function create(string $resourceClass, string $property, array $options = break; } + if ($options['api_allow_update'] ?? false) { + break; + } + $propertyMetadata = $propertyMetadata->withWritable(false); break; diff --git a/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php b/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php index 096c75e31a0..796f0089fd8 100644 --- a/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php +++ b/src/Doctrine/Odm/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactory.php @@ -16,17 +16,19 @@ use ApiPlatform\Doctrine\Odm\State\CollectionProvider; use ApiPlatform\Doctrine\Odm\State\ItemProvider; use ApiPlatform\Doctrine\Odm\State\Options; -use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\DeleteOperationInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\State\Util\StateOptionsTrait; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\Persistence\ManagerRegistry; final class DoctrineMongoDbOdmResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface { + use StateOptionsTrait; + public function __construct(private readonly ManagerRegistry $managerRegistry, private readonly ResourceMetadataCollectionFactoryInterface $decorated) { } @@ -38,18 +40,12 @@ public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = $this->decorated->create($resourceClass); - /** @var ApiResource $resourceMetadata */ foreach ($resourceMetadataCollection as $i => $resourceMetadata) { $operations = $resourceMetadata->getOperations(); if ($operations) { - /** @var Operation $operation */ foreach ($resourceMetadata->getOperations() as $operationName => $operation) { - $documentClass = $operation->getClass(); - if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getDocumentClass()) { - $documentClass = $options->getDocumentClass(); - } - + $documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); if (!$this->managerRegistry->getManagerForClass($documentClass) instanceof DocumentManager) { continue; } diff --git a/src/Doctrine/Odm/Paginator.php b/src/Doctrine/Odm/Paginator.php index b39d6ee1b4f..65370dcdd11 100644 --- a/src/Doctrine/Odm/Paginator.php +++ b/src/Doctrine/Odm/Paginator.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Doctrine\Odm; -use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\State\Pagination\HasNextPagePaginatorInterface; use ApiPlatform\State\Pagination\PaginatorInterface; use Doctrine\ODM\MongoDB\Iterator\Iterator; @@ -25,43 +25,35 @@ * @author Kévin Dunglas * @author Alan Poulain */ -final class Paginator implements \IteratorAggregate, PaginatorInterface, HasNextPagePaginatorInterface +final class Paginator extends AbstractPaginator implements PaginatorInterface, HasNextPagePaginatorInterface { - public const LIMIT_ZERO_MARKER_FIELD = '___'; - public const LIMIT_ZERO_MARKER = 'limit0'; - - private ?\ArrayIterator $iterator = null; - - private readonly int $firstResult; - - private readonly int $maxResults; - private readonly int $totalItems; - public function __construct(private readonly Iterator $mongoDbOdmIterator, private readonly UnitOfWork $unitOfWork, private readonly string $resourceClass, private readonly array $pipeline) + public function __construct(Iterator $mongoDbOdmIterator, UnitOfWork $unitOfWork, string $resourceClass) { - $resultsFacetInfo = $this->getFacetInfo('results'); - $this->getFacetInfo('count'); + $result = $mongoDbOdmIterator->toArray()[0]; - /* - * Since the {@see \MongoDB\Driver\Cursor} class does not expose information about - * skip/limit parameters of the query, the values set in the facet stage are used instead. - */ - $this->firstResult = $this->getStageInfo($resultsFacetInfo, '$skip'); - $this->maxResults = $this->hasLimitZeroStage($resultsFacetInfo) ? 0 : $this->getStageInfo($resultsFacetInfo, '$limit'); - $this->totalItems = $mongoDbOdmIterator->toArray()[0]['count'][0]['count'] ?? 0; - } - - /** - * {@inheritdoc} - */ - public function getCurrentPage(): float - { - if (0 >= $this->maxResults) { - return 1.; + if (array_diff_key(['results' => 1, 'count' => 1, '__api_first_result__' => 1, '__api_max_results__' => 1], $result)) { + throw new RuntimeException('The result of the query must contain only "__api_first_result__", "__api_max_results__", "results" and "count" fields.'); } - return floor($this->firstResult / $this->maxResults) + 1.; + parent::__construct($result); + + // The "count" facet contains the total number of documents, + // it is not set when the query does not return any document + $this->totalItems = $result['count'][0]['count'] ?? 0; + + // The "results" facet contains the returned documents + if ([] === $result['results']) { + $this->count = 0; + $this->iterator = new \ArrayIterator(); + } else { + $this->count = \count($result['results']); + $this->iterator = new \ArrayIterator(array_map( + static fn ($result): object => $unitOfWork->getOrCreateDocument($resourceClass, $result), + $result['results'], + )); + } } /** @@ -76,14 +68,6 @@ public function getLastPage(): float return ceil($this->totalItems / $this->maxResults) ?: 1.; } - /** - * {@inheritdoc} - */ - public function getItemsPerPage(): float - { - return (float) $this->maxResults; - } - /** * {@inheritdoc} */ @@ -92,22 +76,6 @@ public function getTotalItems(): float return (float) $this->totalItems; } - /** - * {@inheritdoc} - */ - public function getIterator(): \Traversable - { - return $this->iterator ?? $this->iterator = new \ArrayIterator(array_map(fn ($result): object => $this->unitOfWork->getOrCreateDocument($this->resourceClass, $result), $this->mongoDbOdmIterator->toArray()[0]['results'])); - } - - /** - * {@inheritdoc} - */ - public function count(): int - { - return is_countable($this->mongoDbOdmIterator->toArray()[0]['results']) ? \count($this->mongoDbOdmIterator->toArray()[0]['results']) : 0; - } - /** * {@inheritdoc} */ @@ -115,47 +83,4 @@ public function hasNextPage(): bool { return $this->getLastPage() > $this->getCurrentPage(); } - - /** - * @throws InvalidArgumentException - */ - private function getFacetInfo(string $field): array - { - foreach ($this->pipeline as $indexStage => $infoStage) { - if (\array_key_exists('$facet', $infoStage)) { - if (!isset($this->pipeline[$indexStage]['$facet'][$field])) { - throw new InvalidArgumentException("\"$field\" facet was not applied to the aggregation pipeline."); - } - - return $this->pipeline[$indexStage]['$facet'][$field]; - } - } - - throw new InvalidArgumentException('$facet stage was not applied to the aggregation pipeline.'); - } - - /** - * @throws InvalidArgumentException - */ - private function getStageInfo(array $resultsFacetInfo, string $stage): int - { - foreach ($resultsFacetInfo as $resultFacetInfo) { - if (isset($resultFacetInfo[$stage])) { - return $resultFacetInfo[$stage]; - } - } - - throw new InvalidArgumentException("$stage stage was not applied to the facet stage of the aggregation pipeline."); - } - - private function hasLimitZeroStage(array $resultsFacetInfo): bool - { - foreach ($resultsFacetInfo as $resultFacetInfo) { - if (self::LIMIT_ZERO_MARKER === ($resultFacetInfo['$match'][self::LIMIT_ZERO_MARKER_FIELD] ?? null)) { - return true; - } - } - - return false; - } } diff --git a/src/Doctrine/Odm/PartialPaginator.php b/src/Doctrine/Odm/PartialPaginator.php new file mode 100644 index 00000000000..fadedc55aaa --- /dev/null +++ b/src/Doctrine/Odm/PartialPaginator.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Odm; + +use ApiPlatform\Metadata\Exception\RuntimeException; +use Doctrine\ODM\MongoDB\Iterator\Iterator; +use Doctrine\ODM\MongoDB\UnitOfWork; + +final class PartialPaginator extends AbstractPaginator +{ + public function __construct(Iterator $mongoDbOdmIterator, UnitOfWork $unitOfWork, string $resourceClass) + { + $result = $mongoDbOdmIterator->toArray()[0]; + + if (array_diff_key(['results' => 1, '__api_first_result__' => 1, '__api_max_results__' => 1], $result)) { + throw new RuntimeException('The result of the query must contain only "__api_first_result__", "__api_max_results__" and "results" fields.'); + } + + parent::__construct($result); + + // The "results" facet contains the returned documents + if ([] === $result['results']) { + $this->count = 0; + $this->iterator = new \ArrayIterator(); + } else { + $this->count = \count($result['results']); + $this->iterator = new \ArrayIterator(array_map( + static fn ($result): object => $unitOfWork->getOrCreateDocument($resourceClass, $result), + $result['results'], + )); + } + } +} diff --git a/src/Doctrine/Odm/PropertyHelperTrait.php b/src/Doctrine/Odm/PropertyHelperTrait.php index e1c7693f2b0..168e0cec6bc 100644 --- a/src/Doctrine/Odm/PropertyHelperTrait.php +++ b/src/Doctrine/Odm/PropertyHelperTrait.php @@ -27,7 +27,7 @@ */ trait PropertyHelperTrait { - abstract protected function getManagerRegistry(): ManagerRegistry; + abstract protected function getManagerRegistry(): ?ManagerRegistry; /** * Splits the given property into parts. @@ -39,9 +39,13 @@ abstract protected function splitPropertyParts(string $property, string $resourc */ protected function getClassMetadata(string $resourceClass): ClassMetadata { - $manager = $this - ->getManagerRegistry() - ->getManagerForClass($resourceClass); + /** + * @var ?ManagerRegistry $managerRegistry + * + * @phpstan-ignore varTag.nativeType (https://github.com/phpstan/phpstan/issues/9515) + */ + $managerRegistry = $this->getManagerRegistry(); + $manager = $managerRegistry?->getManagerForClass($resourceClass); if ($manager) { return $manager->getClassMetadata($resourceClass); diff --git a/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php b/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php index 7967a6f2192..00105f4929e 100644 --- a/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php +++ b/src/Doctrine/Odm/PropertyInfo/DoctrineExtractor.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Doctrine\Odm\PropertyInfo; -use ApiPlatform\Metadata\Util\PropertyInfoToTypeInfoHelper; use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata as MongoDbClassMetadata; use Doctrine\ODM\MongoDB\Types\Type as MongoDbType; @@ -25,6 +24,7 @@ use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Extracts data using Doctrine MongoDB ODM metadata. @@ -41,6 +41,8 @@ public function __construct(private readonly ObjectManager $objectManager) /** * {@inheritdoc} * + * @param string $class + * * @return string[]|null */ public function getProperties($class, array $context = []): ?array @@ -52,13 +54,74 @@ public function getProperties($class, array $context = []): ?array return $metadata->getFieldNames(); } + public function getType(string $class, string $property, array $context = []): ?Type + { + if (null === $metadata = $this->getMetadata($class)) { + return null; + } + + if ($metadata->hasAssociation($property)) { + /** @var class-string|null */ + $class = $metadata->getAssociationTargetClass($property); + + if (null === $class) { + return null; + } + + if ($metadata->isSingleValuedAssociation($property)) { + $nullable = $metadata instanceof MongoDbClassMetadata && $metadata->isNullable($property); + + return $nullable ? Type::nullable(Type::object($class)) : Type::object($class); + } + + return Type::collection(Type::object(Collection::class), Type::object($class), Type::int()); + } + + if (!$metadata->hasField($property)) { + return null; + } + + $typeOfField = $metadata->getTypeOfField($property); + + if (!$typeIdentifier = $this->getTypeIdentifier($typeOfField)) { + return null; + } + + $nullable = $metadata instanceof MongoDbClassMetadata && $metadata->isNullable($property); + $enumType = null; + + if (null !== $enumClass = $metadata instanceof MongoDbClassMetadata ? $metadata->getFieldMapping($property)['enumType'] ?? null : null) { + $enumType = $nullable ? Type::nullable(Type::enum($enumClass)) : Type::enum($enumClass); + } + + $builtinType = $nullable ? Type::nullable(Type::builtin($typeIdentifier)) : Type::builtin($typeIdentifier); + + $type = match ($typeOfField) { + MongoDbType::DATE => Type::object(\DateTime::class), + MongoDbType::DATE_IMMUTABLE => Type::object(\DateTimeImmutable::class), + MongoDbType::HASH => Type::array(), + MongoDbType::COLLECTION => Type::list(), + MongoDbType::INT, MongoDbType::INTEGER, MongoDbType::STRING => $enumType ? $enumType : $builtinType, + default => $builtinType, + }; + + return $nullable ? Type::nullable($type) : $type; + } + /** * {@inheritdoc} * + * // deprecated since 4.2 use "getType" instead + * + * @param string $class + * @param string $property + * * @return LegacyType[]|null */ - public function getTypes(string $class, string $property, array $context = []): ?array + public function getTypes($class, $property, array $context = []): ?array { + trigger_deprecation('api-platform/core', '4.2', 'The "%s()" method is deprecated, use "%s::getType()" instead.', __METHOD__, self::class); + if (null === $metadata = $this->getMetadata($class)) { return null; } @@ -115,7 +178,7 @@ public function getTypes(string $class, string $property, array $context = []): } } - $builtinType = $this->getPhpType($typeOfField); + $builtinType = $this->getNativeTypeLegacy($typeOfField); return $builtinType ? [new LegacyType($builtinType, $nullable)] : null; } @@ -125,6 +188,9 @@ public function getTypes(string $class, string $property, array $context = []): /** * {@inheritdoc} + * + * @param string $class + * @param string $property */ public function isReadable($class, $property, array $context = []): ?bool { @@ -133,6 +199,9 @@ public function isReadable($class, $property, array $context = []): ?bool /** * {@inheritdoc} + * + * @param string $class + * @param string $property */ public function isWritable($class, $property, array $context = []): ?bool { @@ -156,15 +225,23 @@ private function getMetadata(string $class): ?ClassMetadata } } - public function getType(string $class, string $property, array $context = []): ?Type + /** + * Gets the corresponding built-in PHP type identifier. + */ + private function getTypeIdentifier(string $doctrineType): ?TypeIdentifier { - return PropertyInfoToTypeInfoHelper::convertLegacyTypesToType($this->getTypes($class, $property, $context)); + return match ($doctrineType) { + MongoDbType::INTEGER, MongoDbType::INT, MongoDbType::INTID, MongoDbType::KEY => TypeIdentifier::INT, + MongoDbType::FLOAT => TypeIdentifier::FLOAT, + MongoDbType::STRING, MongoDbType::ID, MongoDbType::OBJECTID, MongoDbType::TIMESTAMP, MongoDbType::BINDATA, MongoDbType::BINDATABYTEARRAY, MongoDbType::BINDATACUSTOM, MongoDbType::BINDATAFUNC, MongoDbType::BINDATAMD5, MongoDbType::BINDATAUUID, MongoDbType::BINDATAUUIDRFC4122 => TypeIdentifier::STRING, + MongoDbType::BOOLEAN, MongoDbType::BOOL => TypeIdentifier::BOOL, + MongoDbType::DATE, MongoDbType::DATE_IMMUTABLE => TypeIdentifier::OBJECT, + MongoDbType::HASH, MongoDbType::COLLECTION => TypeIdentifier::ARRAY, + default => null, + }; } - /** - * Gets the corresponding built-in PHP type. - */ - private function getPhpType(string $doctrineType): ?string + private function getNativeTypeLegacy(string $doctrineType): ?string { return match ($doctrineType) { MongoDbType::INTEGER, MongoDbType::INT, MongoDbType::INTID, MongoDbType::KEY => LegacyType::BUILTIN_TYPE_INT, diff --git a/src/Doctrine/Odm/README.md b/src/Doctrine/Odm/README.md new file mode 100644 index 00000000000..020d983f4a4 --- /dev/null +++ b/src/Doctrine/Odm/README.md @@ -0,0 +1,12 @@ +# API Platform - Doctrine MongoDB ODM Support + +Integration for [Doctrine MongoDB ODM](https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/current/index.html) with the [API Platform](https://api-platform.com) framework. + +[Documentation](https://api-platform.com/docs/core/mongodb/) + +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/Doctrine/Odm/State/CollectionProvider.php b/src/Doctrine/Odm/State/CollectionProvider.php index 3c5c923f893..6c68b663f3e 100644 --- a/src/Doctrine/Odm/State/CollectionProvider.php +++ b/src/Doctrine/Odm/State/CollectionProvider.php @@ -20,6 +20,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\Util\StateOptionsTrait; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; use Doctrine\Persistence\ManagerRegistry; @@ -32,6 +33,7 @@ final class CollectionProvider implements ProviderInterface { use LinksHandlerLocatorTrait; use LinksHandlerTrait; + use StateOptionsTrait; /** * @param AggregationCollectionExtensionInterface[] $collectionExtensions @@ -45,10 +47,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable { - $documentClass = $operation->getClass(); - if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getDocumentClass()) { - $documentClass = $options->getDocumentClass(); - } + $documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); /** @var DocumentManager $manager */ $manager = $this->managerRegistry->getManagerForClass($documentClass); @@ -77,6 +76,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $attribute = $operation->getExtraProperties()['doctrine_mongodb'] ?? []; $executeOptions = $attribute['execute_options'] ?? []; - return $aggregationBuilder->hydrate($documentClass)->execute($executeOptions); + return $aggregationBuilder->hydrate($documentClass)->getAggregation($executeOptions)->getIterator(); } } diff --git a/src/Doctrine/Odm/State/ItemProvider.php b/src/Doctrine/Odm/State/ItemProvider.php index d367ea4fa02..b1b8999e38a 100644 --- a/src/Doctrine/Odm/State/ItemProvider.php +++ b/src/Doctrine/Odm/State/ItemProvider.php @@ -20,6 +20,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\Util\StateOptionsTrait; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; use Doctrine\Persistence\ManagerRegistry; @@ -35,6 +36,7 @@ final class ItemProvider implements ProviderInterface { use LinksHandlerLocatorTrait; use LinksHandlerTrait; + use StateOptionsTrait; /** * @param AggregationItemExtensionInterface[] $itemExtensions @@ -48,10 +50,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object { - $documentClass = $operation->getClass(); - if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getDocumentClass()) { - $documentClass = $options->getDocumentClass(); - } + $documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); /** @var DocumentManager $manager */ $manager = $this->managerRegistry->getManagerForClass($documentClass); @@ -84,6 +83,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $executeOptions = $operation->getExtraProperties()['doctrine_mongodb']['execute_options'] ?? []; - return $aggregationBuilder->hydrate($documentClass)->execute($executeOptions)->current() ?: null; + return $aggregationBuilder->hydrate($documentClass)->getAggregation($executeOptions)->getIterator()->current() ?: null; } } diff --git a/src/Doctrine/Odm/State/LinksHandlerInterface.php b/src/Doctrine/Odm/State/LinksHandlerInterface.php index 99e46168324..427bbd0852b 100644 --- a/src/Doctrine/Odm/State/LinksHandlerInterface.php +++ b/src/Doctrine/Odm/State/LinksHandlerInterface.php @@ -16,9 +16,6 @@ use ApiPlatform\Metadata\Operation; use Doctrine\ODM\MongoDB\Aggregation\Builder; -/** - * @experimental - */ interface LinksHandlerInterface { /** diff --git a/src/Doctrine/Odm/State/LinksHandlerTrait.php b/src/Doctrine/Odm/State/LinksHandlerTrait.php index c4e8b651b38..215f0cbb899 100644 --- a/src/Doctrine/Odm/State/LinksHandlerTrait.php +++ b/src/Doctrine/Odm/State/LinksHandlerTrait.php @@ -14,12 +14,12 @@ namespace ApiPlatform\Doctrine\Odm\State; use ApiPlatform\Doctrine\Common\State\LinksHandlerTrait as CommonLinksHandlerTrait; -use ApiPlatform\Exception\RuntimeException; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\Util\StateOptionsTrait; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\DocumentManager; -use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; /** @@ -28,6 +28,7 @@ trait LinksHandlerTrait { use CommonLinksHandlerTrait; + use StateOptionsTrait; private ManagerRegistry $managerRegistry; @@ -57,12 +58,8 @@ private function handleLinks(Builder $aggregationBuilder, array $identifiers, ar /** * @throws RuntimeException */ - private function buildAggregation(string $toClass, array $links, array $identifiers, array $context, array $executeOptions, string $previousAggregationClass, Builder $previousAggregationBuilder, ?Operation $operation = null): Builder + private function buildAggregation(string $toClass, array $links, array $identifiers, array $context, array $executeOptions, string $previousAggregationClass, Builder $previousAggregationBuilder, Operation $operation): Builder { - if (!$operation) { - trigger_deprecation('api-platform/core', '3.2', 'In API Platform 4 the last argument "operation" will be required and this trait will be internal. Use the "handleLinks" feature instead.'); - } - if (\count($links) <= 0) { return $previousAggregationBuilder; } @@ -86,10 +83,8 @@ private function buildAggregation(string $toClass, array $links, array $identifi $manager = $this->managerRegistry->getManagerForClass($aggregationClass); if (!$manager instanceof DocumentManager) { - if ($operation) { - $aggregationClass = $this->getLinkFromClass($link, $operation); - $manager = $this->managerRegistry->getManagerForClass($aggregationClass); - } + $aggregationClass = $this->getLinkFromClass($link, $operation); + $manager = $this->managerRegistry->getManagerForClass($aggregationClass); if (!$manager instanceof DocumentManager) { throw new RuntimeException(\sprintf('The manager for "%s" must be an instance of "%s".', $aggregationClass, DocumentManager::class)); @@ -98,10 +93,6 @@ private function buildAggregation(string $toClass, array $links, array $identifi $classMetadata = $manager->getClassMetadata($aggregationClass); - if (!$classMetadata instanceof ClassMetadata) { - throw new RuntimeException(\sprintf('The class metadata for "%s" must be an instance of "%s".', $aggregationClass, ClassMetadata::class)); - } - $aggregation = $previousAggregationBuilder; if ($aggregationClass !== $previousAggregationClass) { $aggregation = $manager->createAggregationBuilder($aggregationClass); @@ -128,7 +119,7 @@ private function buildAggregation(string $toClass, array $links, array $identifi return $aggregation; } - $results = $aggregation->execute($executeOptions)->toArray(); + $results = $aggregation->getAggregation($executeOptions)->getIterator()->toArray(); $in = []; foreach ($results as $result) { foreach ($result[$lookupPropertyAlias] ?? [] as $lookupResult) { @@ -142,26 +133,18 @@ private function buildAggregation(string $toClass, array $links, array $identifi private function getLinkFromClass(Link $link, Operation $operation): string { + $documentClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); $fromClass = $link->getFromClass(); - if ($fromClass === $operation->getClass() && $documentClass = $this->getStateOptionsDocumentClass($operation)) { + if ($fromClass === $operation->getClass() && $documentClass) { return $documentClass; } $operation = $this->resourceMetadataCollectionFactory->create($fromClass)->getOperation(); - if ($documentClass = $this->getStateOptionsDocumentClass($operation)) { + if ($documentClass = $this->getStateOptionsClass($operation, null, Options::class)) { return $documentClass; } throw new \Exception('Can not found a doctrine class for this link.'); } - - private function getStateOptionsDocumentClass(Operation $operation): ?string - { - if (($options = $operation->getStateOptions()) && $options instanceof Options && $documentClass = $options->getDocumentClass()) { - return $documentClass; - } - - return null; - } } diff --git a/src/Doctrine/Odm/Tests/AppKernel.php b/src/Doctrine/Odm/Tests/AppKernel.php index 1b9481a50b2..0333f06741e 100644 --- a/src/Doctrine/Odm/Tests/AppKernel.php +++ b/src/Doctrine/Odm/Tests/AppKernel.php @@ -18,6 +18,7 @@ use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; /** @@ -42,6 +43,12 @@ public function registerBundles(): array return [ new FrameworkBundle(), new DoctrineMongoDBBundle(), + new class extends Bundle { + public function shutdown(): void + { + restore_exception_handler(); + } + }, ]; } diff --git a/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmFilterTestCase.php b/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmFilterTestCase.php index cce6e2d8d25..3641c4ace94 100644 --- a/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmFilterTestCase.php +++ b/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmFilterTestCase.php @@ -45,9 +45,7 @@ protected function setUp(): void $this->repository = $this->manager->getRepository($this->resourceClass); } - /** - * @dataProvider provideApplyTestData - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideApplyTestData')] public function testApply(?array $properties, array $filterParameters, array $expectedPipeline, ?callable $factory = null, ?string $resourceClass = null): void { $this->doTestApply($properties, $filterParameters, $expectedPipeline, $factory, $resourceClass); diff --git a/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmSetup.php b/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmSetup.php index 0746d8665ff..3ab49d9f4cd 100644 --- a/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmSetup.php +++ b/src/Doctrine/Odm/Tests/DoctrineMongoDbOdmSetup.php @@ -93,7 +93,7 @@ private static function createCacheConfiguration(bool $isDevMode, string $proxyD $namespace .= ':'; } - $cache->setNamespace($namespace.'dc2_'.md5($proxyDir.$hydratorDir).'_'); // to avoid collisions + $cache->setNamespace($namespace.'dc2_'.hash('xxh3', $proxyDir.$hydratorDir).'_'); // to avoid collisions return $cache; } diff --git a/src/Doctrine/Odm/Tests/Extension/FilterExtensionTest.php b/src/Doctrine/Odm/Tests/Extension/FilterExtensionTest.php index d90104ca55d..8c94abafb5a 100644 --- a/src/Doctrine/Odm/Tests/Extension/FilterExtensionTest.php +++ b/src/Doctrine/Odm/Tests/Extension/FilterExtensionTest.php @@ -26,8 +26,6 @@ /** * @author Alan Poulain - * - * @group mongodb */ class FilterExtensionTest extends TestCase { diff --git a/src/Doctrine/Odm/Tests/Extension/OrderExtensionTest.php b/src/Doctrine/Odm/Tests/Extension/OrderExtensionTest.php index db157f582d8..f56aa8cb6e8 100644 --- a/src/Doctrine/Odm/Tests/Extension/OrderExtensionTest.php +++ b/src/Doctrine/Odm/Tests/Extension/OrderExtensionTest.php @@ -28,8 +28,6 @@ /** * @author Alan Poulain - * - * @group mongodb */ class OrderExtensionTest extends TestCase { diff --git a/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php b/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php index ed880c214df..557549898dc 100644 --- a/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php +++ b/src/Doctrine/Odm/Tests/Extension/PaginationExtensionTest.php @@ -14,7 +14,6 @@ namespace ApiPlatform\Doctrine\Odm\Tests\Extension; use ApiPlatform\Doctrine\Odm\Extension\PaginationExtension; -use ApiPlatform\Doctrine\Odm\Paginator; use ApiPlatform\Doctrine\Odm\Tests\DoctrineMongoDbOdmSetup; use ApiPlatform\Doctrine\Odm\Tests\Fixtures\Document\Dummy; use ApiPlatform\Metadata\Exception\InvalidArgumentException; @@ -23,11 +22,12 @@ use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; use Doctrine\ODM\MongoDB\Aggregation\Builder; +use Doctrine\ODM\MongoDB\Aggregation\Stage\AddFields; use Doctrine\ODM\MongoDB\Aggregation\Stage\Count; use Doctrine\ODM\MongoDB\Aggregation\Stage\Facet; -use Doctrine\ODM\MongoDB\Aggregation\Stage\MatchStage as AggregationMatch; use Doctrine\ODM\MongoDB\Aggregation\Stage\Skip; use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Iterator\IterableResult; use Doctrine\ODM\MongoDB\Iterator\Iterator; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; use Doctrine\Persistence\ManagerRegistry; @@ -37,13 +37,12 @@ /** * @author Alan Poulain - * - * @group mongodb */ class PaginationExtensionTest extends TestCase { use ProphecyTrait; + /** @var ObjectProphecy */ private ObjectProphecy $managerRegistryProphecy; /** @@ -324,16 +323,22 @@ public function testGetResult(): void $iteratorProphecy = $this->prophesize(Iterator::class); $iteratorProphecy->toArray()->willReturn([ [ + 'results' => [], 'count' => [ [ 'count' => 9, ], ], + '__api_first_result__' => 3, + '__api_max_results__' => 6, ], ]); + $aggregationProphecy = $this->prophesize(IterableResult::class); + $aggregationProphecy->getIterator()->willReturn($iteratorProphecy->reveal()); + $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->execute([])->willReturn($iteratorProphecy->reveal()); + $aggregationBuilderProphecy->getAggregation([])->willReturn($aggregationProphecy->reveal()); $aggregationBuilderProphecy->getPipeline()->willReturn([ [ '$facet' => [ @@ -346,6 +351,12 @@ public function testGetResult(): void ], ], ], + [ + '$addFields' => [ + '__api_first_result__' => ['$literal' => 3], + '__api_max_results__' => ['$literal' => 6], + ], + ], ]); $paginationExtension = new PaginationExtension( @@ -372,16 +383,22 @@ public function testGetResultWithExecuteOptions(): void $iteratorProphecy = $this->prophesize(Iterator::class); $iteratorProphecy->toArray()->willReturn([ [ + 'results' => [], 'count' => [ [ 'count' => 9, ], ], + '__api_first_result__' => 3, + '__api_max_results__' => 6, ], ]); + $aggregationProphecy = $this->prophesize(IterableResult::class); + $aggregationProphecy->getIterator()->willReturn($iteratorProphecy->reveal()); + $aggregationBuilderProphecy = $this->prophesize(Builder::class); - $aggregationBuilderProphecy->execute(['allowDiskUse' => true])->willReturn($iteratorProphecy->reveal()); + $aggregationBuilderProphecy->getAggregation(['allowDiskUse' => true])->willReturn($aggregationProphecy->reveal()); $aggregationBuilderProphecy->getPipeline()->willReturn([ [ '$facet' => [ @@ -394,6 +411,12 @@ public function testGetResultWithExecuteOptions(): void ], ], ], + [ + '$addFields' => [ + '__api_first_result__' => ['$literal' => 3], + '__api_max_results__' => ['$literal' => 6], + ], + ], ]); $paginationExtension = new PaginationExtension( @@ -409,29 +432,11 @@ public function testGetResultWithExecuteOptions(): void private function mockAggregationBuilder(int $expectedOffset, int $expectedLimit): ObjectProphecy { - $skipProphecy = $this->prophesize(Skip::class); - if ($expectedLimit > 0) { - $skipProphecy->limit($expectedLimit)->shouldBeCalled(); - } else { - $matchProphecy = $this->prophesize(AggregationMatch::class); - $matchProphecy->field(Paginator::LIMIT_ZERO_MARKER_FIELD)->shouldBeCalled()->willReturn($matchProphecy->reveal()); - $matchProphecy->equals(Paginator::LIMIT_ZERO_MARKER)->shouldBeCalled()->willReturn($matchProphecy->reveal()); - $skipProphecy->match()->shouldBeCalled()->willReturn($matchProphecy->reveal()); - } - - $resultsAggregationBuilderProphecy = $this->prophesize(Builder::class); - $resultsAggregationBuilderProphecy->skip($expectedOffset)->shouldBeCalled()->willReturn($skipProphecy->reveal()); - $countProphecy = $this->prophesize(Count::class); - $countAggregationBuilderProphecy = $this->prophesize(Builder::class); $countAggregationBuilderProphecy->count('count')->shouldBeCalled()->willReturn($countProphecy->reveal()); $repositoryProphecy = $this->prophesize(DocumentRepository::class); - $repositoryProphecy->createAggregationBuilder()->shouldBeCalled()->willReturn( - $resultsAggregationBuilderProphecy->reveal(), - $countAggregationBuilderProphecy->reveal() - ); $objectManagerProphecy = $this->prophesize(DocumentManager::class); $objectManagerProphecy->getRepository('Foo')->shouldBeCalled()->willReturn($repositoryProphecy->reveal()); @@ -439,13 +444,40 @@ private function mockAggregationBuilder(int $expectedOffset, int $expectedLimit) $this->managerRegistryProphecy->getManagerForClass('Foo')->shouldBeCalled()->willReturn($objectManagerProphecy->reveal()); $facetProphecy = $this->prophesize(Facet::class); - $facetProphecy->pipeline($skipProphecy)->shouldBeCalled()->willReturn($facetProphecy); - $facetProphecy->pipeline($countProphecy)->shouldBeCalled()->willReturn($facetProphecy); - $facetProphecy->field('count')->shouldBeCalled()->willReturn($facetProphecy); - $facetProphecy->field('results')->shouldBeCalled()->willReturn($facetProphecy); + $addFieldsProphecy = $this->prophesize(AddFields::class); + + if ($expectedLimit > 0) { + $resultsAggregationBuilderProphecy = $this->prophesize(Builder::class); + $repositoryProphecy->createAggregationBuilder()->shouldBeCalled()->willReturn( + $resultsAggregationBuilderProphecy->reveal(), + $countAggregationBuilderProphecy->reveal() + ); + + $skipProphecy = $this->prophesize(Skip::class); + $skipProphecy->limit($expectedLimit)->shouldBeCalled()->willReturn($skipProphecy->reveal()); + $resultsAggregationBuilderProphecy->skip($expectedOffset)->shouldBeCalled()->willReturn($skipProphecy->reveal()); + $facetProphecy->field('results')->shouldBeCalled()->willReturn($facetProphecy); + $facetProphecy->pipeline($skipProphecy)->shouldBeCalled()->willReturn($facetProphecy->reveal()); + } else { + $repositoryProphecy->createAggregationBuilder()->shouldBeCalled()->willReturn( + $countAggregationBuilderProphecy->reveal() + ); + + $addFieldsProphecy->field('results')->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); + $addFieldsProphecy->literal([])->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); + } + + $facetProphecy->field('count')->shouldBeCalled()->willReturn($facetProphecy->reveal()); + $facetProphecy->pipeline($countProphecy)->shouldBeCalled()->willReturn($facetProphecy->reveal()); + + $addFieldsProphecy->field('__api_first_result__')->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); + $addFieldsProphecy->literal($expectedOffset)->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); + $addFieldsProphecy->field('__api_max_results__')->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); + $addFieldsProphecy->literal($expectedLimit)->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); $aggregationBuilderProphecy = $this->prophesize(Builder::class); $aggregationBuilderProphecy->facet()->shouldBeCalled()->willReturn($facetProphecy->reveal()); + $aggregationBuilderProphecy->addFields()->shouldBeCalled()->willReturn($addFieldsProphecy->reveal()); return $aggregationBuilderProphecy; } diff --git a/src/Doctrine/Odm/Tests/Extension/ParameterExtensionTest.php b/src/Doctrine/Odm/Tests/Extension/ParameterExtensionTest.php new file mode 100644 index 00000000000..0188c1c0206 --- /dev/null +++ b/src/Doctrine/Odm/Tests/Extension/ParameterExtensionTest.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Odm\Tests\Extension; + +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; +use ApiPlatform\Doctrine\Odm\Extension\ParameterExtension; +use ApiPlatform\Doctrine\Odm\Filter\FilterInterface; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use Doctrine\Bundle\MongoDBBundle\ManagerRegistry; +use Doctrine\ODM\MongoDB\Aggregation\Builder; +use PHPUnit\Framework\Assert; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; + +class ParameterExtensionTest extends TestCase +{ + public function testApplyToCollectionWithNoParameters(): void + { + $aggregationBuilder = $this->createMock(Builder::class); + $operation = new GetCollection(); + $extension = new ParameterExtension($this->createNonCalledFilterLocator()); + + $context = []; + $extension->applyToCollection($aggregationBuilder, 'SomeClass', $operation, $context); + + $this->assertSame([], $context); + } + + public function testApplyToCollectionWithParameterAndFilter(): void + { + $filterLocator = $this->createMock(ContainerInterface::class); + $filterLocator->expects($this->once())->method('has') + ->with('filter_service_id') + ->willReturn(true); + $filterLocator->expects($this->once())->method('get') + ->with('filter_service_id') + ->willReturn($this->createFilterMock()); + + $aggregationBuilder = $this->createMock(Builder::class); + $operation = (new GetCollection()) + ->withParameters([ + (new QueryParameter( + key: 'param1', + filter: $this->createFilterMock(), + ))->setValue(1), + (new QueryParameter( + key: 'param2', + filter: 'filter_service_id' // From the container + ))->setValue(2), + new QueryParameter( + key: 'param3', + // Filer not called because no value + filter: $this->createFilterMock(false) + ), + new QueryParameter( + key: 'param4', + // Filer not called because no value + filter: 'filter_service_id_not_called' + ), + ]); + $extension = new ParameterExtension($filterLocator); + + $context = []; + $extension->applyToCollection($aggregationBuilder, 'SomeClass', $operation, $context); + + $this->assertSame([], $context); + } + + public function testApplyToCollectionWithLoggerAndManagerRegistry(): void + { + $aggregationBuilder = $this->createMock(Builder::class); + + $filter = new class implements FilterInterface, LoggerAwareInterface, ManagerRegistryAwareInterface { + use BackwardCompatibleFilterDescriptionTrait; + use LoggerAwareTrait; + use ManagerRegistryAwareTrait; + + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + Assert::assertNotNull($this->logger); + Assert::assertNotNull($this->managerRegistry); + Assert::assertSame('SomeClass', $resourceClass); + } + }; + + $operation = (new GetCollection()) + ->withParameters([ + (new QueryParameter( + key: 'param1', + filter: $filter, + ))->setValue(1), + ]); + + $extension = new ParameterExtension( + $this->createNonCalledFilterLocator(), + $this->createMock(ManagerRegistry::class), + $this->createMock(LoggerInterface::class), + ); + $context = []; + $extension->applyToCollection($aggregationBuilder, 'SomeClass', $operation, $context); + + $this->assertSame([], $context); + $this->assertNotNull($filter->getLogger()); + $this->assertNotNull($filter->getManagerRegistry()); + } + + public function testApplyToCollectionPassesContext(): void + { + $aggregationBuilder = $this->createMock(Builder::class); + + $filter = new class implements FilterInterface { + use BackwardCompatibleFilterDescriptionTrait; + + public function apply(Builder $aggregationBuilder, string $resourceClass, ?Operation $operation = null, array &$context = []): void + { + Assert::assertIsArray($context['filters']); + Assert::assertInstanceOf(Parameter::class, $context['parameter']); + $context['check_the_filters'][] = $context['filters']; + } + }; + + $operation = (new GetCollection()) + ->withParameters([ + (new QueryParameter( + key: 'param1', + filter: $filter, + ))->setValue(1), + (new QueryParameter( + key: 'param2', + filter: $filter, + ))->setValue(2), + ]); + + $extension = new ParameterExtension($this->createNonCalledFilterLocator()); + $context = []; + $extension->applyToCollection($aggregationBuilder, 'SomeClass', $operation, $context); + + $this->assertSame([ + 'check_the_filters' => [ + ['param1' => 1], + ['param2' => 2], + ], + ], $context); + } + + private function createFilterMock(bool $expectCall = true): FilterInterface + { + $filter = $this->createMock(FilterInterface::class); + $filter->expects($expectCall ? $this->once() : $this->never()) + ->method('apply'); + + return $filter; + } + + private function createNonCalledFilterLocator(): ContainerInterface + { + $filterLocator = $this->createMock(ContainerInterface::class); + $filterLocator->expects($this->never())->method('has'); + $filterLocator->expects($this->never())->method('get'); + + return $filterLocator; + } +} diff --git a/src/Doctrine/Odm/Tests/Filter/BooleanFilterTest.php b/src/Doctrine/Odm/Tests/Filter/BooleanFilterTest.php index 4dce86ef25d..520f8517ad6 100644 --- a/src/Doctrine/Odm/Tests/Filter/BooleanFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/BooleanFilterTest.php @@ -18,8 +18,6 @@ use ApiPlatform\Doctrine\Odm\Tests\Fixtures\Document\Dummy; /** - * @group mongodb - * * @author Alan Poulain */ class BooleanFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/DateFilterTest.php b/src/Doctrine/Odm/Tests/Filter/DateFilterTest.php index 170082d17dd..e8ab45d62c2 100644 --- a/src/Doctrine/Odm/Tests/Filter/DateFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/DateFilterTest.php @@ -19,8 +19,6 @@ use MongoDB\BSON\UTCDateTime; /** - * @group mongodb - * * @author Alan Poulain */ class DateFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/ExistsFilterTest.php b/src/Doctrine/Odm/Tests/Filter/ExistsFilterTest.php index 0bf87b0a7fe..d6e987ce65b 100644 --- a/src/Doctrine/Odm/Tests/Filter/ExistsFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/ExistsFilterTest.php @@ -19,8 +19,6 @@ use Doctrine\Persistence\ManagerRegistry; /** - * @group mongodb - * * @author Alan Poulain */ class ExistsFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/NumericFilterTest.php b/src/Doctrine/Odm/Tests/Filter/NumericFilterTest.php index 3dd6b22d87f..db6e702afc0 100644 --- a/src/Doctrine/Odm/Tests/Filter/NumericFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/NumericFilterTest.php @@ -18,8 +18,6 @@ use ApiPlatform\Doctrine\Odm\Tests\Fixtures\Document\Dummy; /** - * @group mongodb - * * @author Alan Poulain */ class NumericFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/OrderFilterTest.php b/src/Doctrine/Odm/Tests/Filter/OrderFilterTest.php index 4c6666c3573..24c6a7bad81 100644 --- a/src/Doctrine/Odm/Tests/Filter/OrderFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/OrderFilterTest.php @@ -21,8 +21,6 @@ use Doctrine\Persistence\ManagerRegistry; /** - * @group mongodb - * * @author Alan Poulain */ class OrderFilterTest extends DoctrineMongoDbOdmFilterTestCase @@ -42,6 +40,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -54,6 +53,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -66,6 +66,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -78,6 +79,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -90,6 +92,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -102,6 +105,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -114,6 +118,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -126,6 +131,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -138,6 +144,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -150,6 +157,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -162,6 +170,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -174,6 +183,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -187,6 +197,7 @@ public function testGetDescriptionDefaultFields(): void 'required' => false, 'schema' => [ 'type' => 'string', + 'default' => 'asc', 'enum' => [ 'asc', 'desc', @@ -198,6 +209,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -210,6 +222,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -222,6 +235,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', diff --git a/src/Doctrine/Odm/Tests/Filter/OrderFilterTestTrait.php b/src/Doctrine/Odm/Tests/Filter/OrderFilterTestTrait.php index 1d079f9e1ae..23c959a4bfc 100644 --- a/src/Doctrine/Odm/Tests/Filter/OrderFilterTestTrait.php +++ b/src/Doctrine/Odm/Tests/Filter/OrderFilterTestTrait.php @@ -30,6 +30,7 @@ public function testGetDescription(): void 'required' => false, 'schema' => [ 'type' => 'string', + 'default' => 'asc', 'enum' => [ 'asc', 'desc', @@ -42,6 +43,7 @@ public function testGetDescription(): void 'required' => false, 'schema' => [ 'type' => 'string', + 'default' => 'asc', 'enum' => [ 'asc', 'desc', diff --git a/src/Doctrine/Odm/Tests/Filter/RangeFilterTest.php b/src/Doctrine/Odm/Tests/Filter/RangeFilterTest.php index 51fd7744fe0..4863ae25840 100644 --- a/src/Doctrine/Odm/Tests/Filter/RangeFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/RangeFilterTest.php @@ -18,8 +18,6 @@ use ApiPlatform\Doctrine\Odm\Tests\Fixtures\Document\Dummy; /** - * @group mongodb - * * @author Alan Poulain */ class RangeFilterTest extends DoctrineMongoDbOdmFilterTestCase diff --git a/src/Doctrine/Odm/Tests/Filter/SearchFilterTest.php b/src/Doctrine/Odm/Tests/Filter/SearchFilterTest.php index 307c622145e..6a2f99e143b 100644 --- a/src/Doctrine/Odm/Tests/Filter/SearchFilterTest.php +++ b/src/Doctrine/Odm/Tests/Filter/SearchFilterTest.php @@ -27,8 +27,6 @@ /** * @author Alan Poulain - * - * @group mongodb */ class SearchFilterTest extends DoctrineMongoDbOdmFilterTestCase { diff --git a/src/Doctrine/Odm/Tests/Fixtures/CustomConverter.php b/src/Doctrine/Odm/Tests/Fixtures/CustomConverter.php index 7071d9a202d..417ae97c457 100644 --- a/src/Doctrine/Odm/Tests/Fixtures/CustomConverter.php +++ b/src/Doctrine/Odm/Tests/Fixtures/CustomConverter.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Doctrine\Odm\Tests\Fixtures; -use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -21,7 +20,7 @@ * Custom converter that will only convert a property named "nameConverted" * with the same logic as Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter. */ -class CustomConverter implements AdvancedNameConverterInterface +class CustomConverter implements NameConverterInterface { private NameConverterInterface $nameConverter; diff --git a/src/Doctrine/Odm/Tests/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactoryTest.php b/src/Doctrine/Odm/Tests/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactoryTest.php index 6b98b56a2d4..ac2f212ffc7 100644 --- a/src/Doctrine/Odm/Tests/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactoryTest.php +++ b/src/Doctrine/Odm/Tests/Metadata/Property/DoctrineMongoDbOdmPropertyMetadataFactoryTest.php @@ -24,8 +24,6 @@ use Prophecy\PhpUnit\ProphecyTrait; /** - * @group mongodb - * * @author Alan Poulain */ class DoctrineMongoDbOdmPropertyMetadataFactoryTest extends TestCase diff --git a/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php b/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php index b7c6e87183c..f29bf0684d0 100644 --- a/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php +++ b/src/Doctrine/Odm/Tests/Metadata/Resource/DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest.php @@ -21,7 +21,7 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -34,7 +34,7 @@ final class DoctrineMongoDbOdmResourceCollectionMetadataFactoryTest extends Test { use ProphecyTrait; - private function getResourceMetadataCollectionFactory(Operation $operation) + private function getResourceMetadataCollectionFactory(HttpOperation $operation) { if (!class_exists(DocumentManager::class)) { $this->markTestSkipped('ODM not installed'); @@ -70,10 +70,8 @@ public function testWithoutManager(): void $this->assertNull($resourceMetadataCollection->getOperation('graphql_get')->getProvider()); } - /** - * @dataProvider operationProvider - */ - public function testWithProvider(Operation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void + #[\PHPUnit\Framework\Attributes\DataProvider('operationProvider')] + public function testWithProvider(HttpOperation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void { if (!class_exists(DocumentManager::class)) { $this->markTestSkipped('ODM not installed'); diff --git a/src/Doctrine/Odm/Tests/PaginatorTest.php b/src/Doctrine/Odm/Tests/PaginatorTest.php index 08646af589b..e8746ea7e8a 100644 --- a/src/Doctrine/Odm/Tests/PaginatorTest.php +++ b/src/Doctrine/Odm/Tests/PaginatorTest.php @@ -15,22 +15,18 @@ use ApiPlatform\Doctrine\Odm\Paginator; use ApiPlatform\Doctrine\Odm\Tests\Fixtures\Document\Dummy; -use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\RuntimeException; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Iterator\Iterator; +use PHPUnit\Framework\Attributes\TestWith; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -/** - * @group mongodb - */ class PaginatorTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize(int $firstResult, int $maxResults, int $totalItems, int $currentPage, int $lastPage, bool $hasNextPage): void { $paginator = $this->getPaginator($firstResult, $maxResults, $totalItems); @@ -41,62 +37,24 @@ public function testInitialize(int $firstResult, int $maxResults, int $totalItem $this->assertSame($hasNextPage, $paginator->hasNextPage()); } - public function testInitializeWithFacetStageNotApplied(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('$facet stage was not applied to the aggregation pipeline.'); - - $this->getPaginatorWithMissingStage(); - } - - public function testInitializeWithResultsFacetNotApplied(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('"results" facet was not applied to the aggregation pipeline.'); - - $this->getPaginatorWithMissingStage(true); - } - - public function testInitializeWithCountFacetNotApplied(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('"count" facet was not applied to the aggregation pipeline.'); - - $this->getPaginatorWithMissingStage(true, true); - } - - public function testInitializeWithSkipStageNotApplied(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('$skip stage was not applied to the facet stage of the aggregation pipeline.'); - - $this->getPaginatorWithMissingStage(true, true, true); - } - - public function testInitializeWithLimitStageNotApplied(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('$limit stage was not applied to the facet stage of the aggregation pipeline.'); - - $this->getPaginatorWithMissingStage(true, true, true, true); - } - - public function testInitializeWithLimitZeroStageApplied(): void + public function testInitializeWithNoCount(): void { - $paginator = $this->getPaginator(0, 5, 0, true); + $paginator = $this->getPaginatorWithNoCount(); $this->assertSame(1., $paginator->getCurrentPage()); $this->assertSame(1., $paginator->getLastPage()); - $this->assertSame(0., $paginator->getItemsPerPage()); + $this->assertSame(15., $paginator->getItemsPerPage()); } - public function testInitializeWithNoCount(): void + #[TestWith(['__api_first_result__'])] + #[TestWith(['__api_max_results__'])] + #[TestWith(['results'])] + #[TestWith(['count'])] + public function testInitializeWithMissingResultField(string $missingField): void { - $paginator = $this->getPaginatorWithNoCount(); + $this->expectException(RuntimeException::class); - $this->assertSame(1., $paginator->getCurrentPage()); - $this->assertSame(1., $paginator->getLastPage()); - $this->assertSame(15., $paginator->getItemsPerPage()); + $this->getPaginatorMissingResultField($missingField); } public function testGetIterator(): void @@ -106,30 +64,15 @@ public function testGetIterator(): void $this->assertSame($paginator->getIterator(), $paginator->getIterator(), 'Iterator should be cached'); } - private function getPaginator(int $firstResult = 1, int $maxResults = 15, int $totalItems = 42, bool $limitZero = false): Paginator + private function getPaginator(int $firstResult = 1, int $maxResults = 15, int $totalItems = 42): Paginator { $iterator = $this->prophesize(Iterator::class); - $pipeline = [ - [ - '$facet' => [ - 'results' => [ - ['$skip' => $firstResult], - $limitZero ? ['$match' => [Paginator::LIMIT_ZERO_MARKER_FIELD => Paginator::LIMIT_ZERO_MARKER]] : ['$limit' => $maxResults], - ], - 'count' => [ - ['$count' => 'count'], - ], - ], - ], - ]; $iterator->toArray()->willReturn([ [ - 'count' => [ - [ - 'count' => $totalItems, - ], - ], + 'count' => [['count' => $totalItems]], 'results' => [], + '__api_first_result__' => $firstResult, + '__api_max_results__' => $maxResults, ], ]); @@ -137,68 +80,45 @@ private function getPaginator(int $firstResult = 1, int $maxResults = 15, int $t $config = DoctrineMongoDbOdmSetup::createAttributeMetadataConfiguration([$fixturesPath], true); $documentManager = DocumentManager::create(null, $config); - return new Paginator($iterator->reveal(), $documentManager->getUnitOfWork(), Dummy::class, $pipeline); + return new Paginator($iterator->reveal(), $documentManager->getUnitOfWork(), Dummy::class); } - private function getPaginatorWithMissingStage(bool $facet = false, bool $results = false, bool $count = false, bool $maxResults = false): Paginator + private function getPaginatorWithNoCount(): Paginator { - $pipeline = []; - - if ($facet) { - $pipeline[] = [ - '$facet' => [], - ]; - } - - if ($results) { - $pipeline[0]['$facet']['results'] = []; - } - - if ($count) { - $pipeline[0]['$facet']['count'] = []; - } - - if ($maxResults) { - $pipeline[0]['$facet']['results'][] = ['$skip' => 42]; - } - $iterator = $this->prophesize(Iterator::class); + $iterator->toArray()->willReturn([ + [ + 'count' => [], + 'results' => [], + '__api_first_result__' => 1, + '__api_max_results__' => 15, + ], + ]); $fixturesPath = \dirname((string) (new \ReflectionClass(Dummy::class))->getFileName()); $config = DoctrineMongoDbOdmSetup::createAttributeMetadataConfiguration([$fixturesPath], true); $documentManager = DocumentManager::create(null, $config); - return new Paginator($iterator->reveal(), $documentManager->getUnitOfWork(), Dummy::class, $pipeline); + return new Paginator($iterator->reveal(), $documentManager->getUnitOfWork(), Dummy::class); } - private function getPaginatorWithNoCount($firstResult = 1, $maxResults = 15): Paginator + private function getPaginatorMissingResultField(string $missing): Paginator { $iterator = $this->prophesize(Iterator::class); - $pipeline = [ - [ - '$facet' => [ - 'results' => [ - ['$skip' => $firstResult], - ['$limit' => $maxResults], - ], - 'count' => [ - ['$count' => 'count'], - ], - ], - ], - ]; $iterator->toArray()->willReturn([ - [ - 'count' => [], + array_diff_key([ + 'count' => [['count' => 42]], 'results' => [], - ], + '__api_first_result__' => 1, + '__api_max_results__' => 15, + ], [$missing => 1]), ]); $fixturesPath = \dirname((string) (new \ReflectionClass(Dummy::class))->getFileName()); $config = DoctrineMongoDbOdmSetup::createAttributeMetadataConfiguration([$fixturesPath], true); $documentManager = DocumentManager::create(null, $config); - return new Paginator($iterator->reveal(), $documentManager->getUnitOfWork(), Dummy::class, $pipeline); + return new Paginator($iterator->reveal(), $documentManager->getUnitOfWork(), Dummy::class); } public static function initializeProvider(): array diff --git a/src/Doctrine/Odm/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Doctrine/Odm/Tests/PropertyInfo/DoctrineExtractorTest.php index 08e247ef255..017af790834 100644 --- a/src/Doctrine/Odm/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Doctrine/Odm/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -27,12 +27,12 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Types\Type as MongoDbType; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** - * @group mongodb - * * @author Kévin Dunglas * @author Alan Poulain */ @@ -84,19 +84,25 @@ public function testTestGetPropertiesWithEmbedded(): void ); } - /** - * @dataProvider typesProvider - */ - public function testExtract(string $property, ?array $type = null): void + #[IgnoreDeprecations] + #[\PHPUnit\Framework\Attributes\DataProvider('legacyTypesProvider')] + public function testExtractLegacy(string $property, ?array $type = null): void { $this->assertEquals($type, $this->createExtractor()->getTypes(DoctrineDummy::class, $property)); } - public function testExtractWithEmbedOne(): void + #[\PHPUnit\Framework\Attributes\DataProvider('typesProvider')] + public function testExtract(string $property, ?Type $type): void + { + $this->assertEquals($type, $this->createExtractor()->getType(DoctrineDummy::class, $property)); + } + + #[IgnoreDeprecations] + public function testExtractWithEmbedOneLegacy(): void { $expectedTypes = [ - new Type( - Type::BUILTIN_TYPE_OBJECT, + new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineEmbeddable::class ), @@ -110,16 +116,25 @@ public function testExtractWithEmbedOne(): void $this->assertEquals($expectedTypes, $actualTypes); } - public function testExtractWithEmbedMany(): void + public function testExtractWithEmbedOne(): void + { + $this->assertEquals( + Type::object(DoctrineEmbeddable::class), + $this->createExtractor()->getType(DoctrineWithEmbedded::class, 'embedOne'), + ); + } + + #[IgnoreDeprecations] + public function testExtractWithEmbedManyLegacy(): void { $expectedTypes = [ - new Type( - Type::BUILTIN_TYPE_OBJECT, + new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineEmbeddable::class) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineEmbeddable::class) ), ]; @@ -131,58 +146,75 @@ public function testExtractWithEmbedMany(): void $this->assertEquals($expectedTypes, $actualTypes); } - public function testExtractEnum(): void + public function testExtractWithEmbedMany(): void + { + $this->assertEquals( + Type::collection(Type::object(Collection::class), Type::object(DoctrineEmbeddable::class), Type::int()), + $this->createExtractor()->getType(DoctrineWithEmbedded::class, 'embedMany'), + ); + } + + #[IgnoreDeprecations] + public function testExtractEnumLegacy(): void { - $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString')); - $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt')); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString')); + $this->assertEquals([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt')); $this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumCustom')); } - public static function typesProvider(): array + public function testExtractEnum(): void + { + $this->assertEquals(Type::enum(EnumString::class), $this->createExtractor()->getType(DoctrineEnum::class, 'enumString')); + $this->assertEquals(Type::enum(EnumInt::class), $this->createExtractor()->getType(DoctrineEnum::class, 'enumInt')); + $this->assertNull($this->createExtractor()->getType(DoctrineEnum::class, 'enumCustom')); + } + + #[IgnoreDeprecations] + public static function legacyTypesProvider(): array { return [ - ['id', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['bin', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['binByteArray', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['binCustom', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['binFunc', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['binMd5', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['binUuid', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['binUuidRfc4122', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['timestamp', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['date', [new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTime::class)]], - ['dateImmutable', [new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]], - ['float', [new Type(Type::BUILTIN_TYPE_FLOAT)]], - ['bool', [new Type(Type::BUILTIN_TYPE_BOOL)]], - ['int', [new Type(Type::BUILTIN_TYPE_INT)]], - ['string', [new Type(Type::BUILTIN_TYPE_STRING)]], - ['key', [new Type(Type::BUILTIN_TYPE_INT)]], - ['hash', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]], - ['collection', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT))]], - ['objectId', [new Type(Type::BUILTIN_TYPE_STRING)]], + ['id', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['bin', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['binByteArray', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['binCustom', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['binFunc', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['binMd5', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['binUuid', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['binUuidRfc4122', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['timestamp', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['date', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, \DateTime::class)]], + ['dateImmutable', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]], + ['float', [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT)]], + ['bool', [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL)]], + ['int', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['string', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], + ['key', [new LegacyType(LegacyType::BUILTIN_TYPE_INT)]], + ['hash', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true)]], + ['collection', [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT))]], + ['objectId', [new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]], ['raw', null], - ['foo', [new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class)]], + ['foo', [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class)]], ['bar', [ - new Type( - Type::BUILTIN_TYPE_OBJECT, + new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) ), ], ], ['indexedFoo', [ - new Type( - Type::BUILTIN_TYPE_OBJECT, + new LegacyType( + LegacyType::BUILTIN_TYPE_OBJECT, false, Collection::class, true, - new Type(Type::BUILTIN_TYPE_INT), - new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + new LegacyType(LegacyType::BUILTIN_TYPE_INT), + new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) ), ], ], @@ -191,16 +223,54 @@ public static function typesProvider(): array ]; } + /** + * @return iterable + */ + public static function typesProvider(): iterable + { + yield ['id', Type::string()]; + yield ['bin', Type::string()]; + yield ['binByteArray', Type::string()]; + yield ['binCustom', Type::string()]; + yield ['binFunc', Type::string()]; + yield ['binMd5', Type::string()]; + yield ['binUuid', Type::string()]; + yield ['binUuidRfc4122', Type::string()]; + yield ['timestamp', Type::string()]; + yield ['date', Type::object(\DateTime::class)]; + yield ['dateImmutable', Type::object(\DateTimeImmutable::class)]; + yield ['float', Type::float()]; + yield ['bool', Type::bool()]; + yield ['int', Type::int()]; + yield ['string', Type::string()]; + yield ['key', Type::int()]; + yield ['hash', Type::array()]; + yield ['collection', Type::list()]; + yield ['objectId', Type::string()]; + yield ['raw', null]; + yield ['foo', Type::object(DoctrineRelation::class)]; + yield ['bar', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::int())]; + yield ['indexedFoo', Type::collection(Type::object(Collection::class), Type::object(DoctrineRelation::class), Type::int())]; + yield ['customFoo', null]; + yield ['notMapped', null]; + } + public function testGetPropertiesCatchException(): void { $this->assertNull($this->createExtractor()->getProperties('Not\Exist')); } - public function testGetTypesCatchException(): void + #[IgnoreDeprecations] + public function testGetTypesCatchExceptionLegacy(): void { $this->assertNull($this->createExtractor()->getTypes('Not\Exist', 'baz')); } + public function testGetTypesCatchException(): void + { + $this->assertNull($this->createExtractor()->getType('Not\Exist', 'baz')); + } + public function testGeneratedValueNotWritable(): void { $extractor = $this->createExtractor(); @@ -210,7 +280,8 @@ public function testGeneratedValueNotWritable(): void $this->assertNull($extractor->isReadable(DoctrineGeneratedValue::class, 'foo')); } - public function testGetTypesWithEmbedManyOmittingTargetDocument(): void + #[IgnoreDeprecations] + public function testGetTypesWithEmbedManyOmittingTargetDocumentLegacy(): void { $actualTypes = $this->createExtractor()->getTypes( DoctrineWithEmbedded::class, @@ -220,6 +291,11 @@ public function testGetTypesWithEmbedManyOmittingTargetDocument(): void self::assertNull($actualTypes); } + public function testGetTypesWithEmbedManyOmittingTargetDocument(): void + { + $this->assertNull($this->createExtractor()->getType(DoctrineWithEmbedded::class, 'embedManyOmittingTargetDocument')); + } + private function createExtractor(): DoctrineExtractor { $config = DoctrineMongoDbOdmSetup::createAttributeMetadataConfiguration([__DIR__.\DIRECTORY_SEPARATOR], true); diff --git a/src/Doctrine/Odm/Tests/State/CollectionProviderTest.php b/src/Doctrine/Odm/Tests/State/CollectionProviderTest.php index 5d81c5d90cf..868e2f77921 100644 --- a/src/Doctrine/Odm/Tests/State/CollectionProviderTest.php +++ b/src/Doctrine/Odm/Tests/State/CollectionProviderTest.php @@ -22,6 +22,7 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Iterator\IterableResult; use Doctrine\ODM\MongoDB\Iterator\Iterator; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; use Doctrine\Persistence\ManagerRegistry; @@ -29,14 +30,9 @@ use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -/** - * @group mongodb - */ class CollectionProviderTest extends TestCase { - use ExpectDeprecationTrait; use ProphecyTrait; private ObjectProphecy $managerRegistryProphecy; @@ -55,9 +51,12 @@ public function testGetCollection(): void { $iterator = $this->prophesize(Iterator::class)->reveal(); + $aggregationProphecy = $this->prophesize(IterableResult::class); + $aggregationProphecy->getIterator()->willReturn($iterator); + $aggregationBuilderProphecy = $this->prophesize(Builder::class); $aggregationBuilderProphecy->hydrate(ProviderDocument::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); - $aggregationBuilderProphecy->execute([])->willReturn($iterator)->shouldBeCalled(); + $aggregationBuilderProphecy->getAggregation([])->willReturn($aggregationProphecy)->shouldBeCalled(); $aggregationBuilder = $aggregationBuilderProphecy->reveal(); $repositoryProphecy = $this->prophesize(DocumentRepository::class); @@ -81,9 +80,12 @@ public function testGetCollectionWithExecuteOptions(): void { $iterator = $this->prophesize(Iterator::class)->reveal(); + $aggregationProphecy = $this->prophesize(IterableResult::class); + $aggregationProphecy->getIterator()->willReturn($iterator); + $aggregationBuilderProphecy = $this->prophesize(Builder::class); $aggregationBuilderProphecy->hydrate(ProviderDocument::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); - $aggregationBuilderProphecy->execute(['allowDiskUse' => true])->willReturn($iterator)->shouldBeCalled(); + $aggregationBuilderProphecy->getAggregation(['allowDiskUse' => true])->willReturn($aggregationProphecy)->shouldBeCalled(); $aggregationBuilder = $aggregationBuilderProphecy->reveal(); $repositoryProphecy = $this->prophesize(DocumentRepository::class); @@ -148,9 +150,12 @@ public function testOperationNotFound(): void { $iterator = $this->prophesize(Iterator::class)->reveal(); + $aggregationProphecy = $this->prophesize(IterableResult::class); + $aggregationProphecy->getIterator()->willReturn($iterator); + $aggregationBuilderProphecy = $this->prophesize(Builder::class); $aggregationBuilderProphecy->hydrate(ProviderDocument::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); - $aggregationBuilderProphecy->execute([])->willReturn($iterator)->shouldBeCalled(); + $aggregationBuilderProphecy->getAggregation([])->willReturn($aggregationProphecy)->shouldBeCalled(); $aggregationBuilder = $aggregationBuilderProphecy->reveal(); $repositoryProphecy = $this->prophesize(DocumentRepository::class); diff --git a/src/Doctrine/Odm/Tests/State/ItemProviderTest.php b/src/Doctrine/Odm/Tests/State/ItemProviderTest.php index 721c3fd8fa5..cfa052e19c1 100644 --- a/src/Doctrine/Odm/Tests/State/ItemProviderTest.php +++ b/src/Doctrine/Odm/Tests/State/ItemProviderTest.php @@ -24,6 +24,7 @@ use Doctrine\ODM\MongoDB\Aggregation\Builder; use Doctrine\ODM\MongoDB\Aggregation\Stage\MatchStage as AggregationMatch; use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Iterator\IterableResult; use Doctrine\ODM\MongoDB\Iterator\Iterator; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Repository\DocumentRepository; @@ -32,14 +33,9 @@ use Doctrine\Persistence\ObjectRepository; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; -/** - * @group mongodb - */ class ItemProviderTest extends TestCase { - use ExpectDeprecationTrait; use ProphecyTrait; public function testGetItemSingleIdentifier(): void @@ -54,10 +50,13 @@ public function testGetItemSingleIdentifier(): void $result = new \stdClass(); $iterator->current()->willReturn($result)->shouldBeCalled(); + $aggregationProphecy = $this->prophesize(IterableResult::class); + $aggregationProphecy->getIterator()->willReturn($iterator); + $aggregationBuilderProphecy = $this->prophesize(Builder::class); $aggregationBuilderProphecy->match()->willReturn($matchProphecy->reveal())->shouldBeCalled(); $aggregationBuilderProphecy->hydrate(ProviderDocument::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); - $aggregationBuilderProphecy->execute([])->willReturn($iterator->reveal())->shouldBeCalled(); + $aggregationBuilderProphecy->getAggregation([])->willReturn($aggregationProphecy->reveal())->shouldBeCalled(); $aggregationBuilder = $aggregationBuilderProphecy->reveal(); $managerRegistry = $this->getManagerRegistry(ProviderDocument::class, $aggregationBuilder); @@ -87,10 +86,13 @@ public function testGetItemWithExecuteOptions(): void $result = new \stdClass(); $iterator->current()->willReturn($result)->shouldBeCalled(); + $aggregationProphecy = $this->prophesize(IterableResult::class); + $aggregationProphecy->getIterator()->willReturn($iterator); + $aggregationBuilderProphecy = $this->prophesize(Builder::class); $aggregationBuilderProphecy->match()->willReturn($matchProphecy->reveal())->shouldBeCalled(); $aggregationBuilderProphecy->hydrate(ProviderDocument::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); - $aggregationBuilderProphecy->execute(['allowDiskUse' => true])->willReturn($iterator->reveal())->shouldBeCalled(); + $aggregationBuilderProphecy->getAggregation(['allowDiskUse' => true])->willReturn($aggregationProphecy->reveal())->shouldBeCalled(); $aggregationBuilder = $aggregationBuilderProphecy->reveal(); $managerRegistry = $this->getManagerRegistry(ProviderDocument::class, $aggregationBuilder); @@ -121,10 +123,13 @@ public function testGetItemDoubleIdentifier(): void $result = new \stdClass(); $iterator->current()->willReturn($result)->shouldBeCalled(); + $aggregationProphecy = $this->prophesize(IterableResult::class); + $aggregationProphecy->getIterator()->willReturn($iterator); + $aggregationBuilderProphecy = $this->prophesize(Builder::class); $aggregationBuilderProphecy->match()->willReturn($matchProphecy->reveal())->shouldBeCalled(); $aggregationBuilderProphecy->hydrate(ProviderDocument::class)->willReturn($aggregationBuilderProphecy)->shouldBeCalled(); - $aggregationBuilderProphecy->execute([])->willReturn($iterator->reveal())->shouldBeCalled(); + $aggregationBuilderProphecy->getAggregation([])->willReturn($aggregationProphecy->reveal())->shouldBeCalled(); $aggregationBuilder = $aggregationBuilderProphecy->reveal(); $managerRegistry = $this->getManagerRegistry(ProviderDocument::class, $aggregationBuilder); diff --git a/src/Doctrine/Odm/composer.json b/src/Doctrine/Odm/composer.json index 7e7fc7a7cd2..ba419bb64e5 100644 --- a/src/Doctrine/Odm/composer.json +++ b/src/Doctrine/Odm/composer.json @@ -5,7 +5,10 @@ "keywords": [ "Doctrine", "ODM", - "MongoDB" + "MongoDB", + "API", + "REST", + "GraphQL" ], "homepage": "https://api-platform.com/", "license": "MIT", @@ -21,22 +24,21 @@ } ], "require": { - "php": ">=8.1", - "api-platform/doctrine-common": "*@dev || ^3.1", - "api-platform/metadata": "*@dev || ^3.1", - "api-platform/state": "*@dev || ^3.1", - "doctrine/mongodb-odm": "^2.2", - "doctrine/mongodb-odm-bundle": "^5.0", - "symfony/property-info": "^6.4 || ^7.0" + "php": ">=8.2", + "api-platform/doctrine-common": "^4.2.0@beta", + "api-platform/metadata": "^4.2.0@beta", + "api-platform/state": "^4.1.11", + "doctrine/mongodb-odm": "^2.10", + "symfony/property-info": "^6.4 || ^7.1", + "symfony/type-info": "^7.3" }, "require-dev": { - "api-platform/parameter-validator": "*@dev || ^3.2", "doctrine/doctrine-bundle": "^2.11", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^10.0", + "doctrine/mongodb-odm-bundle": "^5.0", + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "11.5.x-dev", "symfony/cache": "^6.4 || ^7.0", "symfony/framework-bundle": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", "symfony/property-access": "^6.4 || ^7.0", "symfony/serializer": "^6.4 || ^7.0", "symfony/uid": "^6.4 || ^7.0", @@ -59,13 +61,26 @@ }, "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" }, "symfony": { - "require": "^6.4" + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" } }, "scripts": { "test": "./vendor/bin/phpunit" - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] } diff --git a/src/Doctrine/Odm/phpunit.baseline.xml b/src/Doctrine/Odm/phpunit.baseline.xml new file mode 100644 index 00000000000..b0766d7d308 --- /dev/null +++ b/src/Doctrine/Odm/phpunit.baseline.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Doctrine/Odm/phpunit.xml.dist b/src/Doctrine/Odm/phpunit.xml.dist index 44fe7563e18..0d6e7ae01e0 100644 --- a/src/Doctrine/Odm/phpunit.xml.dist +++ b/src/Doctrine/Odm/phpunit.xml.dist @@ -10,7 +10,12 @@ ./Tests/ - + + + trigger_deprecation + Doctrine\Deprecations\Deprecation::trigger + Doctrine\Deprecations\Deprecation::delegateTriggerToBackend + ./ diff --git a/src/Doctrine/Orm/.gitattributes b/src/Doctrine/Orm/.gitattributes index ae3c2e1685a..801f2080d71 100644 --- a/src/Doctrine/Orm/.gitattributes +++ b/src/Doctrine/Orm/.gitattributes @@ -1,2 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore /.gitignore export-ignore /Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/Doctrine/Orm/.github/workflows/close_pr.yml b/src/Doctrine/Orm/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/Doctrine/Orm/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/Doctrine/Orm/Extension/DoctrinePaginatorFactory.php b/src/Doctrine/Orm/Extension/DoctrinePaginatorFactory.php index f7daadb8d35..ed4cf3699e0 100644 --- a/src/Doctrine/Orm/Extension/DoctrinePaginatorFactory.php +++ b/src/Doctrine/Orm/Extension/DoctrinePaginatorFactory.php @@ -13,10 +13,16 @@ namespace ApiPlatform\Doctrine\Orm\Extension; +use Doctrine\ORM\Query; +use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\Tools\Pagination\Paginator; class DoctrinePaginatorFactory { + /** + * @param Query|QueryBuilder $query + * @param bool $fetchJoinCollection + */ public function getPaginator($query, $fetchJoinCollection): Paginator { return new Paginator($query, $fetchJoinCollection); diff --git a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php index aa1e08adc88..bcff2458125 100644 --- a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php +++ b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php @@ -181,7 +181,13 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt $associationAlias = $existingJoin->getAlias(); $isLeftJoin = Join::LEFT_JOIN === $existingJoin->getJoinType(); } else { - $isNullable = $mapping['joinColumns'][0]['nullable'] ?? true; + $joinColumn = $mapping['joinColumns'][0] ?? ['nullable' => true]; + if (\is_array($joinColumn)) { + $isNullable = $joinColumn['nullable'] ?? true; + } else { + $isNullable = $joinColumn->nullable ?? true; + } + $isLeftJoin = false !== $wasLeftJoin || true === $isNullable; $method = $isLeftJoin ? 'leftJoin' : 'innerJoin'; diff --git a/src/Doctrine/Orm/Extension/FilterEagerLoadingExtension.php b/src/Doctrine/Orm/Extension/FilterEagerLoadingExtension.php index daba5d97f9d..8145fa61692 100644 --- a/src/Doctrine/Orm/Extension/FilterEagerLoadingExtension.php +++ b/src/Doctrine/Orm/Extension/FilterEagerLoadingExtension.php @@ -15,7 +15,7 @@ use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\ResourceClassResolverInterface; use Doctrine\ORM\EntityManagerInterface; diff --git a/src/Doctrine/Orm/Extension/FilterExtension.php b/src/Doctrine/Orm/Extension/FilterExtension.php index 298eff2905a..801e9c4a685 100644 --- a/src/Doctrine/Orm/Extension/FilterExtension.php +++ b/src/Doctrine/Orm/Extension/FilterExtension.php @@ -16,7 +16,7 @@ use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; use Psr\Container\ContainerInterface; diff --git a/src/Doctrine/Orm/Extension/OrderExtension.php b/src/Doctrine/Orm/Extension/OrderExtension.php index 00140fdf5c6..1d3deaf0b5b 100644 --- a/src/Doctrine/Orm/Extension/OrderExtension.php +++ b/src/Doctrine/Orm/Extension/OrderExtension.php @@ -15,7 +15,7 @@ use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; diff --git a/src/Doctrine/Orm/Extension/PaginationExtension.php b/src/Doctrine/Orm/Extension/PaginationExtension.php index 80513e45b49..9b4ab0d1f11 100644 --- a/src/Doctrine/Orm/Extension/PaginationExtension.php +++ b/src/Doctrine/Orm/Extension/PaginationExtension.php @@ -17,7 +17,7 @@ use ApiPlatform\Doctrine\Orm\Paginator; use ApiPlatform\Doctrine\Orm\Util\QueryChecker; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\Pagination\Pagination; use Doctrine\ORM\QueryBuilder; diff --git a/src/Doctrine/Orm/Extension/ParameterExtension.php b/src/Doctrine/Orm/Extension/ParameterExtension.php index dce2c6298bb..70533ad584d 100644 --- a/src/Doctrine/Orm/Extension/ParameterExtension.php +++ b/src/Doctrine/Orm/Extension/ParameterExtension.php @@ -13,13 +13,18 @@ namespace ApiPlatform\Doctrine\Orm\Extension; +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; use ApiPlatform\Doctrine\Common\ParameterValueExtractorTrait; +use ApiPlatform\Doctrine\Orm\Filter\AbstractFilter; use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ParameterNotFound; use Doctrine\ORM\QueryBuilder; +use Doctrine\Persistence\ManagerRegistry; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; /** * Reads operation parameters and execute its filter. @@ -30,8 +35,11 @@ final class ParameterExtension implements QueryCollectionExtensionInterface, Que { use ParameterValueExtractorTrait; - public function __construct(private readonly ContainerInterface $filterLocator) - { + public function __construct( + private readonly ContainerInterface $filterLocator, + private readonly ?ManagerRegistry $managerRegistry = null, + private readonly ?LoggerInterface $logger = null, + ) { } /** @@ -40,7 +48,7 @@ public function __construct(private readonly ContainerInterface $filterLocator) private function applyFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { foreach ($operation?->getParameters() ?? [] as $parameter) { - if (!($v = $parameter->getValue()) || $v instanceof ParameterNotFound) { + if (null === ($v = $parameter->getValue()) || $v instanceof ParameterNotFound) { continue; } @@ -49,10 +57,42 @@ private function applyFilter(QueryBuilder $queryBuilder, QueryNameGeneratorInter continue; } - $filter = $this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null; - if ($filter instanceof FilterInterface) { - $filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, ['filters' => $values] + $context); + $filter = match (true) { + $filterId instanceof FilterInterface => $filterId, + \is_string($filterId) && $this->filterLocator->has($filterId) => $this->filterLocator->get($filterId), + default => null, + }; + + if (!$filter instanceof FilterInterface) { + continue; + } + + if ($this->managerRegistry && $filter instanceof ManagerRegistryAwareInterface && !$filter->hasManagerRegistry()) { + $filter->setManagerRegistry($this->managerRegistry); + } + + if ($this->logger && $filter instanceof LoggerAwareInterface && !$filter->hasLogger()) { + $filter->setLogger($this->logger); + } + + if ($filter instanceof AbstractFilter && !$filter->getProperties()) { + $propertyKey = $parameter->getProperty() ?? $parameter->getKey(); + + if (str_contains($propertyKey, ':property')) { + $extraProperties = $parameter->getExtraProperties()['_properties'] ?? []; + foreach (array_keys($extraProperties) as $property) { + $properties[$property] = $parameter->getFilterContext(); + } + } else { + $properties = [$propertyKey => $parameter->getFilterContext()]; + } + + $filter->setProperties($properties ?? []); } + + $filter->apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, + ['filters' => $values, 'parameter' => $parameter] + $context + ); } } diff --git a/src/Doctrine/Orm/Filter/AbstractFilter.php b/src/Doctrine/Orm/Filter/AbstractFilter.php index 4ec704638a7..0f9e7417dd1 100644 --- a/src/Doctrine/Orm/Filter/AbstractFilter.php +++ b/src/Doctrine/Orm/Filter/AbstractFilter.php @@ -13,10 +13,12 @@ namespace ApiPlatform\Doctrine\Orm\Filter; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; use ApiPlatform\Doctrine\Common\Filter\PropertyAwareFilterInterface; use ApiPlatform\Doctrine\Common\PropertyHelperTrait; use ApiPlatform\Doctrine\Orm\PropertyHelperTrait as OrmPropertyHelperTrait; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; @@ -24,14 +26,18 @@ use Psr\Log\NullLogger; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface +abstract class AbstractFilter implements FilterInterface, PropertyAwareFilterInterface, ManagerRegistryAwareInterface { use OrmPropertyHelperTrait; use PropertyHelperTrait; protected LoggerInterface $logger; - public function __construct(protected ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, protected ?array $properties = null, protected ?NameConverterInterface $nameConverter = null) - { + public function __construct( + protected ?ManagerRegistry $managerRegistry = null, + ?LoggerInterface $logger = null, + protected ?array $properties = null, + protected ?NameConverterInterface $nameConverter = null, + ) { $this->logger = $logger ?? new NullLogger(); } @@ -51,31 +57,45 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q * @param class-string $resourceClass * @param array $context */ - abstract protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void; + abstract protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void; + + public function hasManagerRegistry(): bool + { + return $this->managerRegistry instanceof ManagerRegistry; + } - protected function getManagerRegistry(): ManagerRegistry + public function getManagerRegistry(): ManagerRegistry { + if (!$this->hasManagerRegistry()) { + throw new RuntimeException('ManagerRegistry must be initialized before accessing it.'); + } + return $this->managerRegistry; } - protected function getProperties(): ?array + public function setManagerRegistry(ManagerRegistry $managerRegistry): void { - return $this->properties; + $this->managerRegistry = $managerRegistry; } - protected function getLogger(): LoggerInterface + public function getProperties(): ?array { - return $this->logger; + return $this->properties; } /** - * @param string[] $properties + * @param array $properties */ public function setProperties(array $properties): void { $this->properties = $properties; } + protected function getLogger(): LoggerInterface + { + return $this->logger; + } + /** * Determines whether the given property is enabled. */ diff --git a/src/Doctrine/Orm/Filter/BackedEnumFilter.php b/src/Doctrine/Orm/Filter/BackedEnumFilter.php new file mode 100644 index 00000000000..ab39bd0d405 --- /dev/null +++ b/src/Doctrine/Orm/Filter/BackedEnumFilter.php @@ -0,0 +1,192 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Orm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\BackedEnumFilterTrait; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\FieldMapping; +use Doctrine\ORM\Query\Expr\Join; +use Doctrine\ORM\QueryBuilder; + +/** + * The backed enum filter allows you to search on backed enum fields and values. + * + * Note: it is possible to filter on properties and relations too. + * + * Syntax: `?property=foo`. + * + *
+ * + * ```php + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * book.backed_enum_filter + * + * + * + * + * + * ``` + * + *
+ * + * Given that the collection endpoint is `/books`, you can filter books with the following query: `/books?status=published`. + * + * @author Rémi Marseille + */ +final class BackedEnumFilter extends AbstractFilter +{ + use BackedEnumFilterTrait; + + /** + * {@inheritdoc} + */ + protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + if ( + !$this->isPropertyEnabled($property, $resourceClass) + || !$this->isPropertyMapped($property, $resourceClass) + || !$this->isBackedEnumField($property, $resourceClass) + ) { + return; + } + + $values = \is_array($value) ? $value : [$value]; + + $normalizedValues = array_filter(array_map( + fn ($v) => $this->normalizeValue($v, $property), + $values + )); + + if (empty($normalizedValues)) { + return; + } + + $alias = $queryBuilder->getRootAliases()[0]; + $field = $property; + + if ($this->isPropertyNested($property, $resourceClass)) { + [$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::INNER_JOIN); + } + + $valueParameter = $queryNameGenerator->generateParameterName($field); + + if (1 === \count($values)) { + $queryBuilder + ->andWhere(\sprintf('%s.%s = :%s', $alias, $field, $valueParameter)) + ->setParameter($valueParameter, $values[0]); + + return; + } + + $queryBuilder + ->andWhere(\sprintf('%s.%s IN (:%s)', $alias, $field, $valueParameter)) + ->setParameter($valueParameter, $values); + } + + /** + * {@inheritdoc} + */ + protected function isBackedEnumField(string $property, string $resourceClass): bool + { + $propertyParts = $this->splitPropertyParts($property, $resourceClass); + $metadata = $this->getNestedMetadata($resourceClass, $propertyParts['associations']); + + if (!$metadata instanceof ClassMetadata) { + return false; + } + + $fieldMapping = $metadata->fieldMappings[$propertyParts['field']]; + + // Doctrine ORM 2.x returns an array and Doctrine ORM 3.x returns a FieldMapping object + if ($fieldMapping instanceof FieldMapping) { + $fieldMapping = (array) $fieldMapping; + } + + if (!($enumType = $fieldMapping['enumType'] ?? null)) { + return false; + } + + if (!($enumType::cases()[0] ?? null) instanceof \BackedEnum) { + return false; + } + + $this->enumTypes[$property] = $enumType; + + return true; + } +} diff --git a/src/Doctrine/Orm/Filter/BooleanFilter.php b/src/Doctrine/Orm/Filter/BooleanFilter.php index e9f0a8373e0..9fda1f507d8 100644 --- a/src/Doctrine/Orm/Filter/BooleanFilter.php +++ b/src/Doctrine/Orm/Filter/BooleanFilter.php @@ -15,7 +15,9 @@ use ApiPlatform\Doctrine\Common\Filter\BooleanFilterTrait; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -106,7 +108,7 @@ * @author Amrouche Hamza * @author Teoh Han Hui */ -final class BooleanFilter extends AbstractFilter +final class BooleanFilter extends AbstractFilter implements JsonSchemaFilterInterface { use BooleanFilterTrait; @@ -117,7 +119,7 @@ final class BooleanFilter extends AbstractFilter /** * {@inheritdoc} */ - protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { if ( !$this->isPropertyEnabled($property, $resourceClass) @@ -145,4 +147,12 @@ protected function filterProperty(string $property, $value, QueryBuilder $queryB ->andWhere(\sprintf('%s.%s = :%s', $alias, $field, $valueParameter)) ->setParameter($valueParameter, $value); } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'boolean']; + } } diff --git a/src/Doctrine/Orm/Filter/DateFilter.php b/src/Doctrine/Orm/Filter/DateFilter.php index 8533ee34406..b7f7af569b8 100644 --- a/src/Doctrine/Orm/Filter/DateFilter.php +++ b/src/Doctrine/Orm/Filter/DateFilter.php @@ -17,7 +17,12 @@ use ApiPlatform\Doctrine\Common\Filter\DateFilterTrait; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Query\Expr\Join; @@ -120,7 +125,7 @@ * @author Kévin Dunglas * @author Théo FIDRY */ -final class DateFilter extends AbstractFilter implements DateFilterInterface +final class DateFilter extends AbstractFilter implements DateFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface { use DateFilterTrait; @@ -138,11 +143,11 @@ final class DateFilter extends AbstractFilter implements DateFilterInterface /** * {@inheritdoc} */ - protected function filterProperty(string $property, $values, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { - // Expect $values to be an array having the period as keys and the date value as values + // Expect $value to be an array having the period as keys and the date value as values if ( - !\is_array($values) + !\is_array($value) || !$this->isPropertyEnabled($property, $resourceClass) || !$this->isPropertyMapped($property, $resourceClass) || !$this->isDateField($property, $resourceClass) @@ -153,7 +158,7 @@ protected function filterProperty(string $property, $values, QueryBuilder $query $alias = $queryBuilder->getRootAliases()[0]; $field = $property; - if ($this->isPropertyNested($property, $resourceClass)) { + if ($this->isPropertyNested($property, $resourceClass) && \count($value) > 0) { [$alias, $field] = $this->addJoinsForNestedProperty($property, $alias, $queryBuilder, $queryNameGenerator, $resourceClass, Join::INNER_JOIN); } @@ -164,53 +169,53 @@ protected function filterProperty(string $property, $values, QueryBuilder $query $queryBuilder->andWhere($queryBuilder->expr()->isNotNull(\sprintf('%s.%s', $alias, $field))); } - if (isset($values[self::PARAMETER_BEFORE])) { + if (isset($value[self::PARAMETER_BEFORE])) { $this->addWhere( $queryBuilder, $queryNameGenerator, $alias, $field, self::PARAMETER_BEFORE, - $values[self::PARAMETER_BEFORE], + $value[self::PARAMETER_BEFORE], $nullManagement, $type ); } - if (isset($values[self::PARAMETER_STRICTLY_BEFORE])) { + if (isset($value[self::PARAMETER_STRICTLY_BEFORE])) { $this->addWhere( $queryBuilder, $queryNameGenerator, $alias, $field, self::PARAMETER_STRICTLY_BEFORE, - $values[self::PARAMETER_STRICTLY_BEFORE], + $value[self::PARAMETER_STRICTLY_BEFORE], $nullManagement, $type ); } - if (isset($values[self::PARAMETER_AFTER])) { + if (isset($value[self::PARAMETER_AFTER])) { $this->addWhere( $queryBuilder, $queryNameGenerator, $alias, $field, self::PARAMETER_AFTER, - $values[self::PARAMETER_AFTER], + $value[self::PARAMETER_AFTER], $nullManagement, $type ); } - if (isset($values[self::PARAMETER_STRICTLY_AFTER])) { + if (isset($value[self::PARAMETER_STRICTLY_AFTER])) { $this->addWhere( $queryBuilder, $queryNameGenerator, $alias, $field, self::PARAMETER_STRICTLY_AFTER, - $values[self::PARAMETER_STRICTLY_AFTER], + $value[self::PARAMETER_STRICTLY_AFTER], $nullManagement, $type ); @@ -269,4 +274,25 @@ protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterf $queryBuilder->setParameter($valueParameter, $value, $type); } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'string', 'format' => 'date']; + } + + public function getOpenApiParameters(Parameter $parameter): array + { + $in = $parameter instanceof QueryParameter ? 'query' : 'header'; + $key = $parameter->getKey(); + + return [ + new OpenApiParameter(name: $key.'[after]', in: $in), + new OpenApiParameter(name: $key.'[before]', in: $in), + new OpenApiParameter(name: $key.'[strictly_after]', in: $in), + new OpenApiParameter(name: $key.'[strictly_before]', in: $in), + ]; + } } diff --git a/src/Doctrine/Orm/Filter/ExactFilter.php b/src/Doctrine/Orm/Filter/ExactFilter.php new file mode 100644 index 00000000000..37956151713 --- /dev/null +++ b/src/Doctrine/Orm/Filter/ExactFilter.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Orm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; + +/** + * @author Vincent Amstoutz + */ +final class ExactFilter implements FilterInterface, OpenApiParameterFilterInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use OpenApiFilterTrait; + + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + $parameter = $context['parameter']; + $value = $parameter->getValue(); + $property = $parameter->getProperty(); + $alias = $queryBuilder->getRootAliases()[0]; + $parameterName = $queryNameGenerator->generateParameterName($property); + + if (\is_array($value)) { + $queryBuilder + ->{$context['whereClause'] ?? 'andWhere'}(\sprintf('%s.%s IN (:%s)', $alias, $property, $parameterName)); + } else { + $queryBuilder + ->{$context['whereClause'] ?? 'andWhere'}(\sprintf('%s.%s = :%s', $alias, $property, $parameterName)); + } + + $queryBuilder->setParameter($parameterName, $value); + } +} diff --git a/src/Doctrine/Orm/Filter/ExistsFilter.php b/src/Doctrine/Orm/Filter/ExistsFilter.php index 0dde78e9e97..7dbb046630e 100644 --- a/src/Doctrine/Orm/Filter/ExistsFilter.php +++ b/src/Doctrine/Orm/Filter/ExistsFilter.php @@ -15,9 +15,13 @@ use ApiPlatform\Doctrine\Common\Filter\ExistsFilterInterface; use ApiPlatform\Doctrine\Common\Filter\ExistsFilterTrait; +use ApiPlatform\Doctrine\Common\Filter\PropertyPlaceholderOpenApiParameterTrait; use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\ORM\Mapping\AssociationMapping; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; @@ -113,11 +117,12 @@ * * @author Teoh Han Hui */ -final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface +final class ExistsFilter extends AbstractFilter implements ExistsFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface { use ExistsFilterTrait; + use PropertyPlaceholderOpenApiParameterTrait; - public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null) + public function __construct(?ManagerRegistry $managerRegistry = null, ?LoggerInterface $logger = null, ?array $properties = null, string $existsParameterName = self::QUERY_PARAMETER_KEY, ?NameConverterInterface $nameConverter = null) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); @@ -129,6 +134,14 @@ public function __construct(ManagerRegistry $managerRegistry, ?LoggerInterface $ */ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { + $parameter = $context['parameter'] ?? null; + + if (null !== ($value = $context['filters'][$parameter?->getProperty()] ?? null)) { + $this->filterProperty($this->denormalizePropertyName($parameter->getProperty()), $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); + + return; + } + foreach ($context['filters'][$this->existsParameterName] ?? [] as $property => $value) { $this->filterProperty($this->denormalizePropertyName($property), $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); } @@ -137,7 +150,7 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q /** * {@inheritdoc} */ - protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { if ( !$this->isPropertyEnabled($property, $resourceClass) @@ -263,4 +276,12 @@ private function isAssociationNullable(AssociationMapping|array $associationMapp return true; } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'boolean']; + } } diff --git a/src/Doctrine/Orm/Filter/FilterInterface.php b/src/Doctrine/Orm/Filter/FilterInterface.php index 4cfa337dfb6..7539574ff37 100644 --- a/src/Doctrine/Orm/Filter/FilterInterface.php +++ b/src/Doctrine/Orm/Filter/FilterInterface.php @@ -16,6 +16,7 @@ use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Metadata\FilterInterface as BaseFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\ORM\QueryBuilder; /** @@ -27,6 +28,8 @@ interface FilterInterface extends BaseFilterInterface { /** * Applies the filter. + * + * @param array{filters?: array|array, parameter?: Parameter, ...} $context */ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void; } diff --git a/src/Doctrine/Orm/Filter/FreeTextQueryFilter.php b/src/Doctrine/Orm/Filter/FreeTextQueryFilter.php new file mode 100644 index 00000000000..b773f047878 --- /dev/null +++ b/src/Doctrine/Orm/Filter/FreeTextQueryFilter.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Orm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\Operation; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\QueryBuilder; + +final class FreeTextQueryFilter implements FilterInterface, ManagerRegistryAwareInterface, LoggerAwareInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use LoggerAwareTrait; + use ManagerRegistryAwareTrait; + + /** + * @param list $properties an array of properties, defaults to `parameter->getProperties()` + */ + public function __construct(private readonly FilterInterface $filter, private readonly ?array $properties = null) + { + } + + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + if ($this->filter instanceof ManagerRegistryAwareInterface) { + $this->filter->setManagerRegistry($this->getManagerRegistry()); + } + + if ($this->filter instanceof LoggerAwareInterface) { + $this->filter->setLogger($this->getLogger()); + } + + $parameter = $context['parameter']; + $qb = clone $queryBuilder; + $qb->resetDQLPart('where'); + $qb->setParameters(new ArrayCollection()); + foreach ($this->properties ?? $parameter->getProperties() ?? [] as $property) { + $this->filter->apply( + $qb, + $queryNameGenerator, + $resourceClass, + $operation, + ['parameter' => $parameter->withProperty($property)] + $context + ); + } + + $queryBuilder->andWhere($qb->getDQLPart('where')); + $parameters = $queryBuilder->getParameters(); + + foreach ($qb->getParameters() as $p) { + $parameters->add($p); + } + + $queryBuilder->setParameters($parameters); + } +} diff --git a/src/Doctrine/Orm/Filter/IriFilter.php b/src/Doctrine/Orm/Filter/IriFilter.php new file mode 100644 index 00000000000..32acf8f59f1 --- /dev/null +++ b/src/Doctrine/Orm/Filter/IriFilter.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Orm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\ParameterProviderFilterInterface; +use ApiPlatform\State\ParameterProvider\IriConverterParameterProvider; +use Doctrine\ORM\QueryBuilder; + +/** + * @author Vincent Amstoutz + */ +final class IriFilter implements FilterInterface, OpenApiParameterFilterInterface, ParameterProviderFilterInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use OpenApiFilterTrait; + + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + $parameter = $context['parameter']; + $value = $parameter->getValue(); + $property = $parameter->getProperty(); + $alias = $queryBuilder->getRootAliases()[0]; + $parameterName = $queryNameGenerator->generateParameterName($property); + + $queryBuilder->join(\sprintf('%s.%s', $alias, $property), $parameterName); + + if (is_iterable($value)) { + $queryBuilder + ->{$context['whereClause'] ?? 'andWhere'}(\sprintf('%s IN (:%s)', $parameterName, $parameterName)); + $queryBuilder->setParameter($parameterName, $value); + } else { + $queryBuilder + ->{$context['whereClause'] ?? 'andWhere'}(\sprintf('%s = :%s', $parameterName, $parameterName)); + $queryBuilder->setParameter($parameterName, $value); + } + } + + public static function getParameterProvider(): string + { + return IriConverterParameterProvider::class; + } +} diff --git a/src/Doctrine/Orm/Filter/NumericFilter.php b/src/Doctrine/Orm/Filter/NumericFilter.php index 545b552d040..661e96a5a9d 100644 --- a/src/Doctrine/Orm/Filter/NumericFilter.php +++ b/src/Doctrine/Orm/Filter/NumericFilter.php @@ -15,7 +15,9 @@ use ApiPlatform\Doctrine\Common\Filter\NumericFilterTrait; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -106,7 +108,7 @@ * @author Amrouche Hamza * @author Teoh Han Hui */ -final class NumericFilter extends AbstractFilter +final class NumericFilter extends AbstractFilter implements JsonSchemaFilterInterface { use NumericFilterTrait; @@ -126,7 +128,7 @@ final class NumericFilter extends AbstractFilter /** * {@inheritdoc} */ - protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { if ( !$this->isPropertyEnabled($property, $resourceClass) @@ -176,4 +178,9 @@ protected function getType(?string $doctrineType = null): string return 'int'; } + + public function getSchema(Parameter $parameter): array + { + return ['type' => 'number']; + } } diff --git a/src/Doctrine/Orm/Filter/OrFilter.php b/src/Doctrine/Orm/Filter/OrFilter.php new file mode 100644 index 00000000000..d8e020221a7 --- /dev/null +++ b/src/Doctrine/Orm/Filter/OrFilter.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Orm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\LoggerAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareInterface; +use ApiPlatform\Doctrine\Common\Filter\ManagerRegistryAwareTrait; +use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; + +/** + * @author Vincent Amstoutz + * + * @experimental + */ +final class OrFilter implements FilterInterface, OpenApiParameterFilterInterface, ManagerRegistryAwareInterface, LoggerAwareInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use LoggerAwareTrait; + use ManagerRegistryAwareTrait; + use OpenApiFilterTrait; + + public function __construct(private readonly FilterInterface $filter) + { + } + + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + if ($this->filter instanceof ManagerRegistryAwareInterface) { + $this->filter->setManagerRegistry($this->getManagerRegistry()); + } + + if ($this->filter instanceof LoggerAwareInterface) { + $this->filter->setLogger($this->getLogger()); + } + + $this->filter->apply( + $queryBuilder, + $queryNameGenerator, + $resourceClass, + $operation, + ['whereClause' => 'orWhere'] + $context + ); + } +} diff --git a/src/Doctrine/Orm/Filter/OrderFilter.php b/src/Doctrine/Orm/Filter/OrderFilter.php index b2abdabb94c..4962bf26e2c 100644 --- a/src/Doctrine/Orm/Filter/OrderFilter.php +++ b/src/Doctrine/Orm/Filter/OrderFilter.php @@ -15,8 +15,12 @@ use ApiPlatform\Doctrine\Common\Filter\OrderFilterInterface; use ApiPlatform\Doctrine\Common\Filter\OrderFilterTrait; +use ApiPlatform\Doctrine\Common\Filter\PropertyPlaceholderOpenApiParameterTrait; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; @@ -195,11 +199,12 @@ * @author Kévin Dunglas * @author Théo FIDRY */ -final class OrderFilter extends AbstractFilter implements OrderFilterInterface +final class OrderFilter extends AbstractFilter implements OrderFilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface { use OrderFilterTrait; + use PropertyPlaceholderOpenApiParameterTrait; - public function __construct(ManagerRegistry $managerRegistry, string $orderParameterName = 'order', ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null, private readonly ?string $orderNullsComparison = null) + public function __construct(?ManagerRegistry $managerRegistry = null, string $orderParameterName = 'order', ?LoggerInterface $logger = null, ?array $properties = null, ?NameConverterInterface $nameConverter = null, private readonly ?string $orderNullsComparison = null) { if (null !== $properties) { $properties = array_map(static function ($propertyOptions) { @@ -224,12 +229,17 @@ public function __construct(ManagerRegistry $managerRegistry, string $orderParam */ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { - if (isset($context['filters']) && !isset($context['filters'][$this->orderParameterName])) { + if ( + isset($context['filters']) + && (!isset($context['filters'][$this->orderParameterName]) || !\is_array($context['filters'][$this->orderParameterName])) + && !isset($context['parameter']) + ) { return; } - if (!isset($context['filters'][$this->orderParameterName]) || !\is_array($context['filters'][$this->orderParameterName])) { - parent::apply($queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); + $parameter = $context['parameter'] ?? null; + if (null !== ($value = $context['filters'][$parameter?->getProperty()] ?? null)) { + $this->filterProperty($this->denormalizePropertyName($parameter->getProperty()), $value, $queryBuilder, $queryNameGenerator, $resourceClass, $operation, $context); return; } @@ -242,13 +252,13 @@ public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $q /** * {@inheritdoc} */ - protected function filterProperty(string $property, $direction, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { if (!$this->isPropertyEnabled($property, $resourceClass) || !$this->isPropertyMapped($property, $resourceClass)) { return; } - $direction = $this->normalizeValue($direction, $property); + $direction = $this->normalizeValue($value, $property); if (null === $direction) { return; } @@ -271,4 +281,12 @@ protected function filterProperty(string $property, $direction, QueryBuilder $qu $queryBuilder->addOrderBy(\sprintf('%s.%s', $alias, $field), $direction); } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'string', 'enum' => ['asc', 'desc']]; + } } diff --git a/src/Doctrine/Orm/Filter/PartialSearchFilter.php b/src/Doctrine/Orm/Filter/PartialSearchFilter.php new file mode 100644 index 00000000000..90cde75c3fa --- /dev/null +++ b/src/Doctrine/Orm/Filter/PartialSearchFilter.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Orm\Filter; + +use ApiPlatform\Doctrine\Common\Filter\OpenApiFilterTrait; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\BackwardCompatibleFilterDescriptionTrait; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Operation; +use Doctrine\ORM\QueryBuilder; + +/** + * @author Vincent Amstoutz + */ +final class PartialSearchFilter implements FilterInterface, OpenApiParameterFilterInterface +{ + use BackwardCompatibleFilterDescriptionTrait; + use OpenApiFilterTrait; + + public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + { + $parameter = $context['parameter']; + $property = $parameter->getProperty(); + $alias = $queryBuilder->getRootAliases()[0]; + $field = $alias.'.'.$property; + $parameterName = $queryNameGenerator->generateParameterName($property); + $values = $parameter->getValue(); + + if (!is_iterable($values)) { + $queryBuilder->setParameter($parameterName, '%'.strtolower($values).'%'); + + $queryBuilder->{$context['whereClause'] ?? 'andWhere'}($queryBuilder->expr()->like( + 'LOWER('.$field.')', + ':'.$parameterName + )); + + return; + } + + $likeExpressions = []; + foreach ($values as $val) { + $parameterName = $queryNameGenerator->generateParameterName($property); + $likeExpressions[] = $queryBuilder->expr()->like( + 'LOWER('.$field.')', + ':'.$parameterName + ); + $queryBuilder->setParameter($parameterName, '%'.strtolower($val).'%'); + } + + $queryBuilder->{$context['whereClause'] ?? 'andWhere'}( + $queryBuilder->expr()->orX(...$likeExpressions) + ); + } +} diff --git a/src/Doctrine/Orm/Filter/RangeFilter.php b/src/Doctrine/Orm/Filter/RangeFilter.php index 85de7117171..240010077e3 100644 --- a/src/Doctrine/Orm/Filter/RangeFilter.php +++ b/src/Doctrine/Orm/Filter/RangeFilter.php @@ -16,7 +16,11 @@ use ApiPlatform\Doctrine\Common\Filter\RangeFilterInterface; use ApiPlatform\Doctrine\Common\Filter\RangeFilterTrait; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; @@ -105,14 +109,14 @@ * * @author Lee Siong Chan */ -final class RangeFilter extends AbstractFilter implements RangeFilterInterface +final class RangeFilter extends AbstractFilter implements RangeFilterInterface, OpenApiParameterFilterInterface { use RangeFilterTrait; /** * {@inheritdoc} */ - protected function filterProperty(string $property, $values, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + protected function filterProperty(string $property, mixed $values, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { if ( !\is_array($values) @@ -222,4 +226,17 @@ protected function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterf break; } } + + public function getOpenApiParameters(Parameter $parameter): array + { + $in = $parameter instanceof QueryParameter ? 'query' : 'header'; + $key = $parameter->getKey(); + + return [ + new OpenApiParameter(name: $key.'[gt]', in: $in), + new OpenApiParameter(name: $key.'[lt]', in: $in), + new OpenApiParameter(name: $key.'[gte]', in: $in), + new OpenApiParameter(name: $key.'[lte]', in: $in), + ]; + } } diff --git a/src/Doctrine/Orm/Filter/SearchFilter.php b/src/Doctrine/Orm/Filter/SearchFilter.php index a94c8627ec0..a93a8c197c9 100644 --- a/src/Doctrine/Orm/Filter/SearchFilter.php +++ b/src/Doctrine/Orm/Filter/SearchFilter.php @@ -13,8 +13,6 @@ namespace ApiPlatform\Doctrine\Orm\Filter; -use ApiPlatform\Api\IdentifiersExtractorInterface as LegacyIdentifiersExtractorInterface; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Doctrine\Common\Filter\SearchFilterInterface; use ApiPlatform\Doctrine\Common\Filter\SearchFilterTrait; use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; @@ -141,7 +139,7 @@ final class SearchFilter extends AbstractFilter implements SearchFilterInterface public const DOCTRINE_INTEGER_TYPE = Types::INTEGER; - public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface|LegacyIriConverterInterface $iriConverter, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface|null $identifiersExtractor = null, ?NameConverterInterface $nameConverter = null) + public function __construct(ManagerRegistry $managerRegistry, IriConverterInterface $iriConverter, ?PropertyAccessorInterface $propertyAccessor = null, ?LoggerInterface $logger = null, ?array $properties = null, ?IdentifiersExtractorInterface $identifiersExtractor = null, ?NameConverterInterface $nameConverter = null) { parent::__construct($managerRegistry, $logger, $properties, $nameConverter); @@ -150,7 +148,7 @@ public function __construct(ManagerRegistry $managerRegistry, IriConverterInterf $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); } - protected function getIriConverter(): IriConverterInterface|LegacyIriConverterInterface + protected function getIriConverter(): IriConverterInterface { return $this->iriConverter; } @@ -163,7 +161,7 @@ protected function getPropertyAccessor(): PropertyAccessorInterface /** * {@inheritdoc} */ - protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void + protected function filterProperty(string $property, mixed $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void { if ( null === $value diff --git a/src/Doctrine/Orm/Metadata/Property/DoctrineOrmPropertyMetadataFactory.php b/src/Doctrine/Orm/Metadata/Property/DoctrineOrmPropertyMetadataFactory.php index c450e85c1d8..a22e6eb6423 100644 --- a/src/Doctrine/Orm/Metadata/Property/DoctrineOrmPropertyMetadataFactory.php +++ b/src/Doctrine/Orm/Metadata/Property/DoctrineOrmPropertyMetadataFactory.php @@ -56,6 +56,10 @@ public function create(string $resourceClass, string $property, array $options = break; } + if ($options['api_allow_update'] ?? false) { + break; + } + if ($doctrineClassMetadata instanceof ClassMetadata) { $writable = $doctrineClassMetadata->isIdentifierNatural(); } else { @@ -70,7 +74,7 @@ public function create(string $resourceClass, string $property, array $options = if ($doctrineClassMetadata instanceof ClassMetadata && \in_array($property, $doctrineClassMetadata->getFieldNames(), true)) { $fieldMapping = $doctrineClassMetadata->getFieldMapping($property); - if (class_exists(FieldMapping::class) && $fieldMapping instanceof FieldMapping) { + if (class_exists(FieldMapping::class)) { $propertyMetadata = $propertyMetadata->withDefault($fieldMapping->default ?? $propertyMetadata->getDefault()); } else { $propertyMetadata = $propertyMetadata->withDefault($fieldMapping['options']['default'] ?? $propertyMetadata->getDefault()); diff --git a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmLinkFactory.php b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmLinkFactory.php index 1922ccdee44..bf958b80727 100644 --- a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmLinkFactory.php +++ b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmLinkFactory.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Doctrine\Orm\Metadata\Resource; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Metadata; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; @@ -28,7 +27,7 @@ */ final class DoctrineOrmLinkFactory implements LinkFactoryInterface, PropertyLinkFactoryInterface { - public function __construct(private readonly ManagerRegistry $managerRegistry, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly LinkFactoryInterface&PropertyLinkFactoryInterface $linkFactory) + public function __construct(private readonly ManagerRegistry $managerRegistry, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly LinkFactoryInterface&PropertyLinkFactoryInterface $linkFactory) { } diff --git a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php index c1021823235..77155723a89 100644 --- a/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php +++ b/src/Doctrine/Orm/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactory.php @@ -16,17 +16,19 @@ use ApiPlatform\Doctrine\Orm\State\CollectionProvider; use ApiPlatform\Doctrine\Orm\State\ItemProvider; use ApiPlatform\Doctrine\Orm\State\Options; -use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\DeleteOperationInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\State\Util\StateOptionsTrait; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; final class DoctrineOrmResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface { + use StateOptionsTrait; + public function __construct(private readonly ManagerRegistry $managerRegistry, private readonly ResourceMetadataCollectionFactoryInterface $decorated) { } @@ -38,17 +40,12 @@ public function create(string $resourceClass): ResourceMetadataCollection { $resourceMetadataCollection = $this->decorated->create($resourceClass); - /** @var ApiResource $resourceMetadata */ foreach ($resourceMetadataCollection as $i => $resourceMetadata) { $operations = $resourceMetadata->getOperations(); if ($operations) { - /** @var Operation $operation */ foreach ($resourceMetadata->getOperations() as $operationName => $operation) { - $entityClass = $operation->getClass(); - if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) { - $entityClass = $options->getEntityClass(); - } + $entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); if (!$this->managerRegistry->getManagerForClass($entityClass) instanceof EntityManagerInterface) { continue; @@ -64,10 +61,7 @@ public function create(string $resourceClass): ResourceMetadataCollection if ($graphQlOperations) { foreach ($graphQlOperations as $operationName => $graphQlOperation) { - $entityClass = $graphQlOperation->getClass(); - if (($options = $graphQlOperation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) { - $entityClass = $options->getEntityClass(); - } + $entityClass = $this->getStateOptionsClass($graphQlOperation, $graphQlOperation->getClass(), Options::class); if (!$this->managerRegistry->getManagerForClass($entityClass) instanceof EntityManagerInterface) { continue; diff --git a/src/Doctrine/Orm/PropertyHelperTrait.php b/src/Doctrine/Orm/PropertyHelperTrait.php index 8ed61ca62ea..d9376bc7ff6 100644 --- a/src/Doctrine/Orm/PropertyHelperTrait.php +++ b/src/Doctrine/Orm/PropertyHelperTrait.php @@ -15,7 +15,7 @@ use ApiPlatform\Doctrine\Orm\Util\QueryBuilderHelper; use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; -use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use Doctrine\ORM\Mapping\ClassMetadata as ClassMetadataInfo; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; @@ -29,7 +29,7 @@ */ trait PropertyHelperTrait { - abstract protected function getManagerRegistry(): ManagerRegistry; + abstract protected function getManagerRegistry(): ?ManagerRegistry; /** * Splits the given property into parts. diff --git a/src/Doctrine/Orm/README.md b/src/Doctrine/Orm/README.md new file mode 100644 index 00000000000..ca9c9ab91b5 --- /dev/null +++ b/src/Doctrine/Orm/README.md @@ -0,0 +1,12 @@ +# API Platform - Doctrine ORM Support + +Integration for [Doctrine ORM](https://www.doctrine-project.org/projects/doctrine-orm/en/current/index.html) with the [API Platform](https://api-platform.com) framework. + +[Documentation](https://api-platform.com/docs/core/getting-started/) + +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/Doctrine/Orm/State/CollectionProvider.php b/src/Doctrine/Orm/State/CollectionProvider.php index ab2cb5a0810..3815447a8d3 100644 --- a/src/Doctrine/Orm/State/CollectionProvider.php +++ b/src/Doctrine/Orm/State/CollectionProvider.php @@ -21,6 +21,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\Util\StateOptionsTrait; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; use Psr\Container\ContainerInterface; @@ -35,6 +36,7 @@ final class CollectionProvider implements ProviderInterface { use LinksHandlerLocatorTrait; use LinksHandlerTrait; + use StateOptionsTrait; /** * @param QueryCollectionExtensionInterface[] $collectionExtensions @@ -48,10 +50,7 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable { - $entityClass = $operation->getClass(); - if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) { - $entityClass = $options->getEntityClass(); - } + $entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); /** @var EntityManagerInterface $manager */ $manager = $this->managerRegistry->getManagerForClass($entityClass); diff --git a/src/Doctrine/Orm/State/ItemProvider.php b/src/Doctrine/Orm/State/ItemProvider.php index 0a7f4862c89..b201d03b7d0 100644 --- a/src/Doctrine/Orm/State/ItemProvider.php +++ b/src/Doctrine/Orm/State/ItemProvider.php @@ -21,6 +21,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\Util\StateOptionsTrait; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ManagerRegistry; use Psr\Container\ContainerInterface; @@ -35,6 +36,7 @@ final class ItemProvider implements ProviderInterface { use LinksHandlerLocatorTrait; use LinksHandlerTrait; + use StateOptionsTrait; /** * @param QueryItemExtensionInterface[] $itemExtensions @@ -48,13 +50,13 @@ public function __construct(ResourceMetadataCollectionFactoryInterface $resource public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object { - $entityClass = $operation->getClass(); - if (($options = $operation->getStateOptions()) && $options instanceof Options && $options->getEntityClass()) { - $entityClass = $options->getEntityClass(); - } + $entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); - /** @var EntityManagerInterface $manager */ + /** @var EntityManagerInterface|null $manager */ $manager = $this->managerRegistry->getManagerForClass($entityClass); + if (null === $manager) { + throw new RuntimeException(\sprintf('No manager found for class "%s". Are you sure it\'s an entity?', $entityClass)); + } $fetchData = $context['fetch_data'] ?? true; if (!$fetchData && \array_key_exists('id', $uriVariables)) { diff --git a/src/Doctrine/Orm/State/LinksHandlerInterface.php b/src/Doctrine/Orm/State/LinksHandlerInterface.php index 98b75c078ef..5372d52ec86 100644 --- a/src/Doctrine/Orm/State/LinksHandlerInterface.php +++ b/src/Doctrine/Orm/State/LinksHandlerInterface.php @@ -17,9 +17,6 @@ use ApiPlatform\Metadata\Operation; use Doctrine\ORM\QueryBuilder; -/** - * @experimental - */ interface LinksHandlerInterface { /** diff --git a/src/Doctrine/Orm/State/LinksHandlerTrait.php b/src/Doctrine/Orm/State/LinksHandlerTrait.php index 9a088a4e87a..d44805a07b5 100644 --- a/src/Doctrine/Orm/State/LinksHandlerTrait.php +++ b/src/Doctrine/Orm/State/LinksHandlerTrait.php @@ -17,6 +17,7 @@ use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\Util\StateOptionsTrait; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ManagerRegistry; @@ -27,6 +28,7 @@ trait LinksHandlerTrait { use CommonLinksHandlerTrait; + use StateOptionsTrait; private ManagerRegistry $managerRegistry; @@ -159,25 +161,12 @@ private function handleLinks(QueryBuilder $queryBuilder, array $identifiers, Que private function getLinkFromClass(Link $link, Operation $operation): string { $fromClass = $link->getFromClass(); - if ($fromClass === $operation->getClass() && $entityClass = $this->getStateOptionsEntityClass($operation)) { + if ($fromClass === $operation->getClass() && $entityClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class)) { return $entityClass; } $operation = $this->resourceMetadataCollectionFactory->create($fromClass)->getOperation(); - if ($entityClass = $this->getStateOptionsEntityClass($operation)) { - return $entityClass; - } - - throw new \Exception('Can not found a doctrine class for this link.'); - } - - private function getStateOptionsEntityClass(Operation $operation): ?string - { - if (($options = $operation->getStateOptions()) && $options instanceof Options && $entityClass = $options->getEntityClass()) { - return $entityClass; - } - - return null; + return $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); } } diff --git a/src/Doctrine/Orm/State/Options.php b/src/Doctrine/Orm/State/Options.php index 397d2da758a..3a9a46c3825 100644 --- a/src/Doctrine/Orm/State/Options.php +++ b/src/Doctrine/Orm/State/Options.php @@ -42,17 +42,4 @@ public function withEntityClass(?string $entityClass): self return $self; } - - public function getHandleLinks(): mixed - { - return $this->handleLinks; - } - - public function withHandleLinks(mixed $handleLinks): self - { - $self = clone $this; - $self->handleLinks = $handleLinks; - - return $self; - } } diff --git a/src/Doctrine/Orm/Tests/AppKernel.php b/src/Doctrine/Orm/Tests/AppKernel.php index 29229e2f5bd..94b796bb140 100644 --- a/src/Doctrine/Orm/Tests/AppKernel.php +++ b/src/Doctrine/Orm/Tests/AppKernel.php @@ -18,6 +18,7 @@ use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; /** @@ -43,6 +44,12 @@ public function registerBundles(): array new FrameworkBundle(), new DoctrineBundle(), new TestBundle(), + new class extends Bundle { + public function shutdown(): void + { + restore_exception_handler(); + } + }, ]; } diff --git a/src/Doctrine/Orm/Tests/DoctrineOrmFilterTestCase.php b/src/Doctrine/Orm/Tests/DoctrineOrmFilterTestCase.php index f091da00923..1b8b25cfa10 100644 --- a/src/Doctrine/Orm/Tests/DoctrineOrmFilterTestCase.php +++ b/src/Doctrine/Orm/Tests/DoctrineOrmFilterTestCase.php @@ -45,9 +45,7 @@ protected function setUp(): void $this->repository = $this->managerRegistry->getManagerForClass(Dummy::class)->getRepository(Dummy::class); } - /** - * @dataProvider provideApplyTestData - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideApplyTestData')] public function testApply(?array $properties, array $filterParameters, string $expectedDql, ?array $expectedParameters = null, ?callable $factory = null, ?string $resourceClass = null): void { $this->doTestApply($properties, $filterParameters, $expectedDql, $expectedParameters, $factory, $resourceClass); diff --git a/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php b/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php index 2bd173db6ca..2ad7a74d335 100644 --- a/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php +++ b/src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php @@ -35,6 +35,7 @@ use ApiPlatform\Metadata\Property\PropertyNameCollection; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\JoinColumn; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use PHPUnit\Framework\TestCase; @@ -94,8 +95,8 @@ public function testApplyToCollection(): void $classMetadataProphecy = $this->prophesize(ClassMetadata::class); $classMetadataProphecy->associationMappings = [ - 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], - 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => RelatedDummy::class], + 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: true)], 'targetEntity' => RelatedDummy::class], + 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => RelatedDummy::class], ]; $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -181,9 +182,9 @@ public function testApplyToItem(): void $classMetadataProphecy = $this->prophesize(ClassMetadata::class); $classMetadataProphecy->associationMappings = [ - 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], - 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => UnknownDummy::class], - 'relatedDummy3' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinTable' => ['joinColumns' => [['nullable' => false]]], 'targetEntity' => UnknownDummy::class], + 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: true)], 'targetEntity' => RelatedDummy::class], + 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => UnknownDummy::class], + 'relatedDummy3' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinTable' => ['joinColumns' => [new JoinColumn(nullable: false)]], 'targetEntity' => UnknownDummy::class], 'relatedDummy4' => ['fetch' => ClassMetadata::FETCH_EAGER, 'targetEntity' => UnknownDummy::class], 'relatedDummy5' => ['fetch' => ClassMetadata::FETCH_LAZY, 'targetEntity' => UnknownDummy::class], 'singleInheritanceRelation' => ['fetch' => ClassMetadata::FETCH_EAGER, 'targetEntity' => AbstractDummy::class], @@ -200,7 +201,7 @@ public function testApplyToItem(): void $relatedClassMetadataProphecy->hasField('embeddedDummy.name')->willReturn(true)->shouldBeCalled(); $relatedClassMetadataProphecy->associationMappings = [ - 'relation' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => UnknownDummy::class], + 'relation' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => UnknownDummy::class], 'thirdLevel' => ['fetch' => ClassMetadata::FETCH_EAGER, 'targetEntity' => ThirdLevel::class, 'sourceEntity' => RelatedDummy::class, 'inversedBy' => 'relatedDummies', 'type' => ClassMetadata::TO_ONE], ]; @@ -361,13 +362,13 @@ public function testMaxJoinsReached(): void $classMetadataProphecy = $this->prophesize(ClassMetadata::class); $classMetadataProphecy->associationMappings = [ - 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => RelatedDummy::class], + 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => RelatedDummy::class], ]; $classMetadataProphecy->hasField('relatedDummy')->willReturn(true); $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); $relatedClassMetadataProphecy->associationMappings = [ - 'dummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => Dummy::class], + 'dummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => Dummy::class], ]; $relatedClassMetadataProphecy->hasField('dummy')->willReturn(true); @@ -410,13 +411,13 @@ public function testMaxDepth(): void $classMetadataProphecy = $this->prophesize(ClassMetadata::class); $classMetadataProphecy->associationMappings = [ - 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => RelatedDummy::class], + 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => RelatedDummy::class], ]; $classMetadataProphecy->hasField('relatedDummy')->willReturn(true); $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); $relatedClassMetadataProphecy->associationMappings = [ - 'dummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => Dummy::class], + 'dummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => Dummy::class], ]; $relatedClassMetadataProphecy->hasField('dummy')->willReturn(true); @@ -471,7 +472,7 @@ public function testForceEager(): void $classMetadataProphecy = $this->prophesize(ClassMetadata::class); $classMetadataProphecy->associationMappings = [ - 'relation' => ['fetch' => ClassMetadata::FETCH_LAZY, 'targetEntity' => UnknownDummy::class, 'joinColumns' => [['nullable' => false]]], + 'relation' => ['fetch' => ClassMetadata::FETCH_LAZY, 'targetEntity' => UnknownDummy::class, 'joinColumns' => [new JoinColumn(nullable: false)]], ]; $unknownClassMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -577,7 +578,7 @@ public function testResourceClassNotFoundExceptionPropertyNameCollection(): void $classMetadataProphecy = $this->prophesize(ClassMetadata::class); $classMetadataProphecy->associationMappings = [ - 'relation' => ['fetch' => ClassMetadata::FETCH_LAZY, 'targetEntity' => UnknownDummy::class, 'joinColumns' => [['nullable' => false]]], + 'relation' => ['fetch' => ClassMetadata::FETCH_LAZY, 'targetEntity' => UnknownDummy::class, 'joinColumns' => [new JoinColumn(nullable: false)]], ]; $emProphecy = $this->prophesize(EntityManager::class); $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); @@ -751,8 +752,8 @@ public function testApplyToCollectionNoPartial(): void $classMetadataProphecy = $this->prophesize(ClassMetadata::class); $classMetadataProphecy->associationMappings = [ - 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], - 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => RelatedDummy::class], + 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: true)], 'targetEntity' => RelatedDummy::class], + 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => RelatedDummy::class], ]; $emProphecy = $this->prophesize(EntityManager::class); @@ -796,8 +797,8 @@ public function testApplyToCollectionWithANonReadableButFetchEagerProperty(): vo $classMetadataProphecy = $this->prophesize(ClassMetadata::class); $classMetadataProphecy->associationMappings = [ - 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => RelatedDummy::class], - 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => RelatedDummy::class], + 'relatedDummy' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: true)], 'targetEntity' => RelatedDummy::class], + 'relatedDummy2' => ['fetch' => ClassMetadata::FETCH_EAGER, 'joinColumns' => [new JoinColumn(nullable: false)], 'targetEntity' => RelatedDummy::class], ]; $emProphecy = $this->prophesize(EntityManager::class); @@ -821,9 +822,7 @@ public function testApplyToCollectionWithANonReadableButFetchEagerProperty(): vo $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); } - /** - * @dataProvider provideExistingJoinCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideExistingJoinCases')] public function testApplyToCollectionWithExistingJoin(string $joinType): void { $context = ['groups' => ['foo']]; diff --git a/src/Doctrine/Orm/Tests/Extension/PaginationExtensionTest.php b/src/Doctrine/Orm/Tests/Extension/PaginationExtensionTest.php index 64e505ce9bc..2b8bcc51142 100644 --- a/src/Doctrine/Orm/Tests/Extension/PaginationExtensionTest.php +++ b/src/Doctrine/Orm/Tests/Extension/PaginationExtensionTest.php @@ -355,9 +355,7 @@ public function testGetResultWithoutDistinct(): void $this->assertFalse($query->getHint(CountWalker::HINT_DISTINCT)); } - /** - * @dataProvider fetchJoinCollectionProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('fetchJoinCollectionProvider')] public function testGetResultWithFetchJoinCollection(bool $paginationFetchJoinCollection, array $context, bool $expected): void { $dummyMetadata = new ClassMetadata(Dummy::class); @@ -405,9 +403,7 @@ public static function fetchJoinCollectionProvider(): array ]; } - /** - * @dataProvider fetchUseOutputWalkersProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('fetchUseOutputWalkersProvider')] public function testGetResultWithUseOutputWalkers(bool $paginationUseOutputWalkers, array $context, bool $expected): void { $dummyMetadata = new ClassMetadata(Dummy::class); diff --git a/src/Doctrine/Orm/Tests/Filter/BackedEnumFilterTest.php b/src/Doctrine/Orm/Tests/Filter/BackedEnumFilterTest.php new file mode 100644 index 00000000000..5f782daa457 --- /dev/null +++ b/src/Doctrine/Orm/Tests/Filter/BackedEnumFilterTest.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Orm\Tests\Filter; + +use ApiPlatform\Doctrine\Orm\Filter\BackedEnumFilter; +use ApiPlatform\Doctrine\Orm\Tests\DoctrineOrmFilterTestCase; +use ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\Dummy; + +/** + * @author Rémi Marseille + */ +final class BackedEnumFilterTest extends DoctrineOrmFilterTestCase +{ + use BackedEnumFilterTestTrait; + + protected string $filterClass = BackedEnumFilter::class; + + public static function provideApplyTestData(): array + { + return array_merge_recursive( + self::provideApplyTestArguments(), + [ + 'valid case' => [ + \sprintf('SELECT o FROM %s o WHERE o.dummyBackedEnum = :dummyBackedEnum_p1', Dummy::class), + ], + 'invalid case' => [ + \sprintf('SELECT o FROM %s o', Dummy::class), + ], + 'valid case for nested property' => [ + \sprintf('SELECT o FROM %s o INNER JOIN o.relatedDummy relatedDummy_a1 WHERE relatedDummy_a1.dummyBackedEnum = :dummyBackedEnum_p1', Dummy::class), + ], + 'invalid case for nested property' => [ + \sprintf('SELECT o FROM %s o', Dummy::class), + ], + 'valid case (multiple values)' => [ + \sprintf('SELECT o FROM %s o WHERE o.dummyBackedEnum IN (:dummyBackedEnum_p1)', Dummy::class), + [ + 'dummyBackedEnum_p1' => [ + 'one', + 'two', + ], + ], + ], + ] + ); + } +} diff --git a/src/Doctrine/Orm/Tests/Filter/BackedEnumFilterTestTrait.php b/src/Doctrine/Orm/Tests/Filter/BackedEnumFilterTestTrait.php new file mode 100644 index 00000000000..bb864d16331 --- /dev/null +++ b/src/Doctrine/Orm/Tests/Filter/BackedEnumFilterTestTrait.php @@ -0,0 +1,146 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Orm\Tests\Filter; + +/** + * @author Rémi Marseille + */ +trait BackedEnumFilterTestTrait +{ + public function testGetDescription(): void + { + $filter = $this->buildFilter([ + 'id' => null, + 'name' => null, + 'foo' => null, + 'dummyBackedEnum' => null, + ]); + + $this->assertEquals([ + 'dummyBackedEnum' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + 'is_collection' => false, + 'schema' => [ + 'type' => 'string', + 'enum' => ['one', 'two'], + ], + ], + 'dummyBackedEnum[]' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + 'is_collection' => true, + 'schema' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'enum' => ['one', 'two'], + ], + ], + ], + ], $filter->getDescription($this->resourceClass)); + } + + public function testGetDescriptionDefaultFields(): void + { + $filter = $this->buildFilter(); + + $this->assertEquals([ + 'dummyBackedEnum' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + 'is_collection' => false, + 'schema' => [ + 'type' => 'string', + 'enum' => ['one', 'two'], + ], + ], + 'dummyBackedEnum[]' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + 'is_collection' => true, + 'schema' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'enum' => ['one', 'two'], + ], + ], + ], + ], $filter->getDescription($this->resourceClass)); + } + + private static function provideApplyTestArguments(): array + { + return [ + 'valid case' => [ + [ + 'id' => null, + 'name' => null, + 'dummyBackedEnum' => null, + ], + [ + 'dummyBackedEnum' => 'one', + ], + ], + 'invalid case' => [ + [ + 'id' => null, + 'name' => null, + 'dummyBackedEnum' => null, + ], + [ + 'dummyBackedEnum' => 'zero', + ], + ], + 'valid case for nested property' => [ + [ + 'id' => null, + 'name' => null, + 'relatedDummy.dummyBackedEnum' => null, + ], + [ + 'relatedDummy.dummyBackedEnum' => 'two', + ], + ], + 'invalid case for nested property' => [ + [ + 'id' => null, + 'name' => null, + 'relatedDummy.dummyBackedEnum' => null, + ], + [ + 'relatedDummy.dummyBackedEnum' => 'foo', + ], + ], + 'valid case (multiple values)' => [ + [ + 'id' => null, + 'name' => null, + 'dummyBackedEnum' => null, + ], + [ + 'dummyBackedEnum' => [ + 'one', + 'two', + ], + ], + ], + ]; + } +} diff --git a/src/Doctrine/Orm/Tests/Filter/ExistsFilterTest.php b/src/Doctrine/Orm/Tests/Filter/ExistsFilterTest.php index fa9f0a71378..168714ea0ed 100644 --- a/src/Doctrine/Orm/Tests/Filter/ExistsFilterTest.php +++ b/src/Doctrine/Orm/Tests/Filter/ExistsFilterTest.php @@ -88,6 +88,11 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'bool', 'required' => false, ], + 'exists[dummyBackedEnum]' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'bool', + 'required' => false, + ], ], $filter->getDescription($this->resourceClass)); } diff --git a/src/Doctrine/Orm/Tests/Filter/OrderFilterTest.php b/src/Doctrine/Orm/Tests/Filter/OrderFilterTest.php index d3e4c77cc30..20e3293aa54 100644 --- a/src/Doctrine/Orm/Tests/Filter/OrderFilterTest.php +++ b/src/Doctrine/Orm/Tests/Filter/OrderFilterTest.php @@ -40,6 +40,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -52,6 +53,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -64,6 +66,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -76,6 +79,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -88,6 +92,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -100,6 +105,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -112,6 +118,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -124,6 +131,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -136,6 +144,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -148,6 +157,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -160,6 +170,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -172,6 +183,20 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', + 'type' => 'string', + 'enum' => [ + 'asc', + 'desc', + ], + ], + ], + 'order[dummyBackedEnum]' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -187,6 +212,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -199,6 +225,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -211,6 +238,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -223,6 +251,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -235,6 +264,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -247,6 +277,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -259,6 +290,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -271,6 +303,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -283,6 +316,7 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', diff --git a/src/Doctrine/Orm/Tests/Filter/OrderFilterTestTrait.php b/src/Doctrine/Orm/Tests/Filter/OrderFilterTestTrait.php index 2409ae8b083..b125895b609 100644 --- a/src/Doctrine/Orm/Tests/Filter/OrderFilterTestTrait.php +++ b/src/Doctrine/Orm/Tests/Filter/OrderFilterTestTrait.php @@ -29,6 +29,7 @@ public function testGetDescription(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', @@ -41,6 +42,7 @@ public function testGetDescription(): void 'type' => 'string', 'required' => false, 'schema' => [ + 'default' => 'asc', 'type' => 'string', 'enum' => [ 'asc', diff --git a/src/Doctrine/Orm/Tests/Filter/RangeFilterTest.php b/src/Doctrine/Orm/Tests/Filter/RangeFilterTest.php index 3fbdb9ffb67..4c0370b60d3 100644 --- a/src/Doctrine/Orm/Tests/Filter/RangeFilterTest.php +++ b/src/Doctrine/Orm/Tests/Filter/RangeFilterTest.php @@ -331,6 +331,31 @@ public function testGetDescriptionDefaultFields(): void 'type' => 'string', 'required' => false, ], + 'dummyBackedEnum[between]' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + ], + 'dummyBackedEnum[gt]' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + ], + 'dummyBackedEnum[gte]' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + ], + 'dummyBackedEnum[lt]' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + ], + 'dummyBackedEnum[lte]' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + ], ], $filter->getDescription($this->resourceClass)); } diff --git a/src/Doctrine/Orm/Tests/Filter/SearchFilterTest.php b/src/Doctrine/Orm/Tests/Filter/SearchFilterTest.php index 0b11fb16b45..5bc48cc983e 100644 --- a/src/Doctrine/Orm/Tests/Filter/SearchFilterTest.php +++ b/src/Doctrine/Orm/Tests/Filter/SearchFilterTest.php @@ -211,6 +211,20 @@ public function testGetDescriptionDefaultFields(): void 'strategy' => 'exact', 'is_collection' => true, ], + 'dummyBackedEnum' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + 'is_collection' => false, + ], + 'dummyBackedEnum[]' => [ + 'property' => 'dummyBackedEnum', + 'type' => 'string', + 'required' => false, + 'strategy' => 'exact', + 'is_collection' => true, + ], ], $filter->getDescription($this->resourceClass)); } diff --git a/src/Doctrine/Orm/Tests/Fixtures/CustomConverter.php b/src/Doctrine/Orm/Tests/Fixtures/CustomConverter.php index e0a06d84a4e..da479c7ab5c 100644 --- a/src/Doctrine/Orm/Tests/Fixtures/CustomConverter.php +++ b/src/Doctrine/Orm/Tests/Fixtures/CustomConverter.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Doctrine\Orm\Tests\Fixtures; -use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -21,7 +20,7 @@ * Custom converter that will only convert a property named "nameConverted" * with the same logic as Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter. */ -class CustomConverter implements AdvancedNameConverterInterface +class CustomConverter implements NameConverterInterface { private NameConverterInterface $nameConverter; diff --git a/src/Doctrine/Orm/Tests/Fixtures/Entity/Dummy.php b/src/Doctrine/Orm/Tests/Fixtures/Entity/Dummy.php index 00df229df21..f5a29cc0d03 100644 --- a/src/Doctrine/Orm/Tests/Fixtures/Entity/Dummy.php +++ b/src/Doctrine/Orm/Tests/Fixtures/Entity/Dummy.php @@ -27,7 +27,7 @@ * * @author Kévin Dunglas */ -#[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false, 'rfc_7807_compliant_errors' => false])] +#[ApiResource(filters: ['my_dummy.backed_enum', 'my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false])] #[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ORM\Entity] @@ -74,6 +74,9 @@ class Dummy #[ORM\Column(nullable: true)] public $dummy; + #[ORM\Column(enumType: DummyBackedEnum::class, nullable: true)] + public DummyBackedEnum $dummyBackedEnum; + /** * @var bool|null A dummy boolean */ diff --git a/src/Doctrine/Orm/Tests/Fixtures/Entity/DummyBackedEnum.php b/src/Doctrine/Orm/Tests/Fixtures/Entity/DummyBackedEnum.php new file mode 100644 index 00000000000..b5d76c05570 --- /dev/null +++ b/src/Doctrine/Orm/Tests/Fixtures/Entity/DummyBackedEnum.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity; + +enum DummyBackedEnum: string +{ + case One = 'one'; + case Two = 'two'; +} diff --git a/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedDummy.php b/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedDummy.php index 9fbe05383ac..4cf6f92c78d 100644 --- a/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedDummy.php +++ b/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedDummy.php @@ -94,6 +94,9 @@ class RelatedDummy extends ParentDummy implements \Stringable #[Groups(['fakemanytomany', 'friends'])] public Collection|iterable $relatedToDummyFriend; + #[ORM\Column(enumType: DummyBackedEnum::class, nullable: true)] + public DummyBackedEnum $dummyBackedEnum; + /** * @var bool|null A dummy bool */ diff --git a/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedToDummyFriend.php b/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedToDummyFriend.php index 3e95aa912b4..e0482b113df 100644 --- a/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedToDummyFriend.php +++ b/src/Doctrine/Orm/Tests/Fixtures/Entity/RelatedToDummyFriend.php @@ -24,7 +24,7 @@ /** * Related To Dummy Friend represent an association table for a manytomany relation. */ -#[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'], extraProperties: ['rfc_7807_compliant_errors' => false])] +#[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'])] #[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] #[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] diff --git a/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmLinkFactoryTest.php b/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmLinkFactoryTest.php index 3c16c0fbb6e..65b5eee9b12 100644 --- a/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmLinkFactoryTest.php +++ b/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmLinkFactoryTest.php @@ -13,7 +13,6 @@ namespace ApiPlatform\Doctrine\Orm\Tests\Metadata\Resource; -use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\Doctrine\Orm\Metadata\Resource\DoctrineOrmLinkFactory; use ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\Dummy; use ApiPlatform\Doctrine\Orm\Tests\Fixtures\Entity\RelatedDummy; @@ -25,6 +24,7 @@ use ApiPlatform\Metadata\Property\PropertyNameCollection; use ApiPlatform\Metadata\Resource\Factory\LinkFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\PropertyLinkFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\Persistence\ManagerRegistry; diff --git a/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php b/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php index 9904186a2bc..966b8982cf4 100644 --- a/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php +++ b/src/Doctrine/Orm/Tests/Metadata/Resource/DoctrineOrmResourceCollectionMetadataFactoryTest.php @@ -21,7 +21,7 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; @@ -34,7 +34,7 @@ class DoctrineOrmResourceCollectionMetadataFactoryTest extends TestCase { use ProphecyTrait; - private function getResourceMetadataCollectionFactory(Operation $operation) + private function getResourceMetadataCollectionFactory(HttpOperation $operation) { $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $resourceMetadataCollectionFactory->create($operation->getClass())->willReturn(new ResourceMetadataCollection($operation->getClass(), [ @@ -62,10 +62,8 @@ public function testWithoutManager(): void $this->assertNull($resourceMetadataCollection->getOperation('graphql_get')->getProvider()); } - /** - * @dataProvider operationProvider - */ - public function testWithProvider(Operation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void + #[\PHPUnit\Framework\Attributes\DataProvider('operationProvider')] + public function testWithProvider(HttpOperation $operation, ?string $expectedProvider = null, ?string $expectedProcessor = null): void { $objectManager = $this->prophesize(EntityManagerInterface::class); $managerRegistry = $this->prophesize(ManagerRegistry::class); diff --git a/src/Doctrine/Orm/Tests/PaginatorTest.php b/src/Doctrine/Orm/Tests/PaginatorTest.php index b5a347a6b93..62010032848 100644 --- a/src/Doctrine/Orm/Tests/PaginatorTest.php +++ b/src/Doctrine/Orm/Tests/PaginatorTest.php @@ -30,9 +30,7 @@ class PaginatorTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider initializeProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('initializeProvider')] public function testInitialize(int $firstResult, int $maxResults, int $totalItems, int $currentPage, int $lastPage, bool $hasNextPage): void { $paginator = $this->getPaginator($firstResult, $maxResults, $totalItems); diff --git a/src/Doctrine/Orm/Tests/State/CollectionProviderTest.php b/src/Doctrine/Orm/Tests/State/CollectionProviderTest.php index 77dbb3062ed..9837c79999d 100644 --- a/src/Doctrine/Orm/Tests/State/CollectionProviderTest.php +++ b/src/Doctrine/Orm/Tests/State/CollectionProviderTest.php @@ -138,7 +138,7 @@ public function testCannotCreateQueryBuilder(): void public function testHandleLinksCallable(): void { - $class = 'foo'; + $class = \stdClass::class; $resourceMetadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); $query = $this->createStub($this->getQueryClass()); $query->method('getResult')->willReturn([]); diff --git a/src/Doctrine/Orm/Tests/State/ItemProviderTest.php b/src/Doctrine/Orm/Tests/State/ItemProviderTest.php index 35432196d42..ec730378939 100644 --- a/src/Doctrine/Orm/Tests/State/ItemProviderTest.php +++ b/src/Doctrine/Orm/Tests/State/ItemProviderTest.php @@ -328,7 +328,7 @@ class_exists(ManyToOneAssociationMapping::class) ? public function testHandleLinksCallable(): void { - $class = 'foo'; + $class = \stdClass::class; $resourceMetadata = $this->createStub(ResourceMetadataCollectionFactoryInterface::class); $query = $this->createStub($this->getQueryClass()); $query->method('getOneOrNullResult')->willReturn(null); diff --git a/src/Doctrine/Orm/Tests/Util/QueryBuilderHelperTest.php b/src/Doctrine/Orm/Tests/Util/QueryBuilderHelperTest.php index 7ce2b446b99..488d217743f 100644 --- a/src/Doctrine/Orm/Tests/Util/QueryBuilderHelperTest.php +++ b/src/Doctrine/Orm/Tests/Util/QueryBuilderHelperTest.php @@ -29,9 +29,7 @@ class QueryBuilderHelperTest extends TestCase { use ProphecyTrait; - /** - * @dataProvider provideAddJoinOnce - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideAddJoinOnce')] public function testAddJoinOnce(?string $originAliasForJoinOnce, string $expectedAlias): void { $queryBuilder = new QueryBuilder($this->prophesize(EntityManagerInterface::class)->reveal()); @@ -57,9 +55,7 @@ public function testAddJoinOnce(?string $originAliasForJoinOnce, string $expecte $queryBuilder->getDQLPart('join')[$originAliasForJoinOnce ?? 'f'][0]->getAlias()); } - /** - * @dataProvider provideAddJoinOnce - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideAddJoinOnce')] public function testAddJoinOnceWithSpecifiedNewAlias(): void { $queryBuilder = new QueryBuilder($this->prophesize(EntityManagerInterface::class)->reveal()); diff --git a/src/Doctrine/Orm/Util/QueryBuilderHelper.php b/src/Doctrine/Orm/Util/QueryBuilderHelper.php index 54956fcc595..52bccafce9e 100644 --- a/src/Doctrine/Orm/Util/QueryBuilderHelper.php +++ b/src/Doctrine/Orm/Util/QueryBuilderHelper.php @@ -186,13 +186,7 @@ public static function getExistingJoin(QueryBuilder $queryBuilder, string $alias */ private static function mapRootAliases(array $rootAliases, array $rootEntities): array { - /** @var false|array $aliasMap */ - $aliasMap = array_combine($rootAliases, $rootEntities); - if (false === $aliasMap) { - throw new \LogicException('Number of root aliases and root entities do not match.'); - } - - return $aliasMap; + return array_combine($rootAliases, $rootEntities); } /** diff --git a/src/Doctrine/Orm/composer.json b/src/Doctrine/Orm/composer.json index e02d885dcca..8fb65b145c7 100644 --- a/src/Doctrine/Orm/composer.json +++ b/src/Doctrine/Orm/composer.json @@ -4,7 +4,10 @@ "type": "library", "keywords": [ "Doctrine", - "ORM" + "ORM", + "API", + "REST", + "GraphQL" ], "homepage": "https://api-platform.com/", "license": "MIT", @@ -20,25 +23,24 @@ } ], "require": { - "php": ">=8.1", - "api-platform/doctrine-common": "*@dev || ^3.1", - "api-platform/metadata": "*@dev || ^3.1", - "api-platform/state": "*@dev || ^3.1", - "doctrine/doctrine-bundle": "^2.11", + "php": ">=8.2", + "api-platform/doctrine-common": "^4.2.0-alpha.3@alpha", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "doctrine/orm": "^2.17 || ^3.0", - "symfony/property-info": "^6.4 || ^7.0" + "symfony/type-info": "^7.3" }, "require-dev": { - "api-platform/parameter-validator": "*@dev || ^3.2", - "phpspec/prophecy-phpunit": "^2.0", - "phpunit/phpunit": "^10.0", + "doctrine/doctrine-bundle": "^2.11", + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "11.5.x-dev", "ramsey/uuid": "^4.7", "ramsey/uuid-doctrine": "^2.0", "symfony/cache": "^6.4 || ^7.0", "symfony/framework-bundle": "^6.4 || ^7.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", "symfony/property-access": "^6.4 || ^7.0", "symfony/serializer": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.1", "symfony/uid": "^6.4 || ^7.0", "symfony/validator": "^6.4 || ^7.0", "symfony/yaml": "^6.4 || ^7.0" @@ -59,13 +61,26 @@ }, "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" }, "symfony": { - "require": "^6.4" + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" } }, "scripts": { "test": "./vendor/bin/phpunit" - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] } diff --git a/src/Doctrine/Orm/phpunit.xml.dist b/src/Doctrine/Orm/phpunit.xml.dist index 913b48c606a..12b5bda4b9e 100644 --- a/src/Doctrine/Orm/phpunit.xml.dist +++ b/src/Doctrine/Orm/phpunit.xml.dist @@ -11,6 +11,11 @@ + + trigger_deprecation + Doctrine\Deprecations\Deprecation::trigger + Doctrine\Deprecations\Deprecation::delegateTriggerToBackend + ./ diff --git a/src/Documentation/.gitattributes b/src/Documentation/.gitattributes index ae3c2e1685a..801f2080d71 100644 --- a/src/Documentation/.gitattributes +++ b/src/Documentation/.gitattributes @@ -1,2 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore /.gitignore export-ignore /Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/Documentation/.github/workflows/close_pr.yml b/src/Documentation/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/Documentation/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/Documentation/Action/DocumentationAction.php b/src/Documentation/Action/DocumentationAction.php deleted file mode 100644 index e507041b6a7..00000000000 --- a/src/Documentation/Action/DocumentationAction.php +++ /dev/null @@ -1,132 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Documentation\Action; - -use ApiPlatform\Documentation\Documentation; -use ApiPlatform\Documentation\DocumentationInterface; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; -use ApiPlatform\Metadata\Util\ContentNegotiationTrait; -use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; -use ApiPlatform\OpenApi\OpenApi; -use ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer; -use ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer; -use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer; -use ApiPlatform\State\ProcessorInterface; -use ApiPlatform\State\ProviderInterface; -use Negotiation\Negotiator; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; - -/** - * Generates the API documentation. - * - * @deprecated use ApiPlatform\Symfony\DocumentationAction instead - * - * @author Amrouche Hamza - */ -final class DocumentationAction -{ - use ContentNegotiationTrait; - - public function __construct( - private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, - private readonly string $title = '', - private readonly string $description = '', - private readonly string $version = '', - private readonly ?OpenApiFactoryInterface $openApiFactory = null, - private readonly ?ProviderInterface $provider = null, - private readonly ?ProcessorInterface $processor = null, - ?Negotiator $negotiator = null, - private readonly array $documentationFormats = [OpenApiNormalizer::JSON_FORMAT => ['application/vnd.openapi+json'], OpenApiNormalizer::FORMAT => ['application/json']], - ) { - $this->negotiator = $negotiator ?? new Negotiator(); - } - - /** - * @return DocumentationInterface|OpenApi|Response - */ - public function __invoke(?Request $request = null) - { - if (null === $request) { - return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version); - } - - $context = [ - 'api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY), - 'base_url' => $request->getBaseUrl(), - 'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION), - ]; - $request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context); - $format = $this->getRequestFormat($request, $this->documentationFormats); - - if (null !== $this->openApiFactory && ('html' === $format || OpenApiNormalizer::FORMAT === $format || OpenApiNormalizer::JSON_FORMAT === $format || OpenApiNormalizer::YAML_FORMAT === $format)) { - return $this->getOpenApiDocumentation($context, $format, $request); - } - - return $this->getHydraDocumentation($context, $request); - } - - /** - * @param array $context - */ - private function getOpenApiDocumentation(array $context, string $format, Request $request): OpenApi|Response - { - if ($this->provider && $this->processor) { - $context['request'] = $request; - $operation = new Get( - class: OpenApi::class, - read: true, - serialize: true, - provider: fn () => $this->openApiFactory->__invoke($context), - normalizationContext: [ - ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null, - LegacyOpenApiNormalizer::SPEC_VERSION => $context['spec_version'] ?? null, - ], - outputFormats: $this->documentationFormats - ); - - if ('html' === $format) { - $operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true); - } - - return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context); - } - - return $this->openApiFactory->__invoke($context); - } - - /** - * TODO: the logic behind the Hydra Documentation is done in a ApiPlatform\Hydra\Serializer\DocumentationNormalizer. - * We should transform this to a provider, it'd improve performances also by a bit. - * - * @param array $context - */ - private function getHydraDocumentation(array $context, Request $request): DocumentationInterface|Response - { - if ($this->provider && $this->processor) { - $context['request'] = $request; - $operation = new Get( - class: Documentation::class, - read: true, - serialize: true, - provider: fn () => new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version) - ); - - return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context); - } - - return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version); - } -} diff --git a/src/Documentation/Action/EntrypointAction.php b/src/Documentation/Action/EntrypointAction.php deleted file mode 100644 index 70677c59821..00000000000 --- a/src/Documentation/Action/EntrypointAction.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Documentation\Action; - -use ApiPlatform\Documentation\Entrypoint; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceNameCollection; -use ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer; -use ApiPlatform\State\ProcessorInterface; -use ApiPlatform\State\ProviderInterface; -use Symfony\Component\HttpFoundation\Request; - -/** - * Generates the API entrypoint. - * - * @deprecated use ApiPlatform\Symfony\EntrypointAction instead - * - * @author Kévin Dunglas - */ -final class EntrypointAction -{ - private static ResourceNameCollection $resourceNameCollection; - - public function __construct( - private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, - private readonly ProviderInterface $provider, - private readonly ProcessorInterface $processor, - private readonly array $documentationFormats = [], - ) { - } - - public function __invoke(Request $request) - { - static::$resourceNameCollection = $this->resourceNameCollectionFactory->create(); - $context = [ - 'request' => $request, - 'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION), - ]; - $request->attributes->set('_api_platform_disable_listeners', true); - $operation = new Get( - outputFormats: $this->documentationFormats, - read: true, - serialize: true, - class: Entrypoint::class, - provider: [self::class, 'provide'] - ); - $request->attributes->set('_api_operation', $operation); - $body = $this->provider->provide($operation, [], $context); - $operation = $request->attributes->get('_api_operation'); - - return $this->processor->process($body, $operation, [], $context); - } - - public static function provide(): Entrypoint - { - return new Entrypoint(static::$resourceNameCollection); - } -} diff --git a/src/Documentation/README.md b/src/Documentation/README.md new file mode 100644 index 00000000000..c934d777c32 --- /dev/null +++ b/src/Documentation/README.md @@ -0,0 +1,12 @@ +# API Platform - Documentation + +The API documentation component of the [API Platform](https://api-platform.com) framework. + +[Documentation](https://api-platform.com/docs/core/) + +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/Documentation/composer.json b/src/Documentation/composer.json index 2b67643f070..93a3278c130 100644 --- a/src/Documentation/composer.json +++ b/src/Documentation/composer.json @@ -20,11 +20,31 @@ } ], "require": { - "api-platform/metadata": "*@dev || ^3.1" + "php": ">=8.2", + "api-platform/metadata": "^4.1.11" }, "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" + }, + "symfony": { + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" + } + }, + "require-dev": { + "phpunit/phpunit": "11.5.x-dev" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" } - } + ] } diff --git a/src/Elasticsearch/.gitattributes b/src/Elasticsearch/.gitattributes index ae3c2e1685a..801f2080d71 100644 --- a/src/Elasticsearch/.gitattributes +++ b/src/Elasticsearch/.gitattributes @@ -1,2 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore /.gitignore export-ignore /Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/Elasticsearch/.github/workflows/close_pr.yml b/src/Elasticsearch/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/Elasticsearch/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/Elasticsearch/Extension/SortExtension.php b/src/Elasticsearch/Extension/SortExtension.php index 013d269ca48..e327f7908f4 100644 --- a/src/Elasticsearch/Extension/SortExtension.php +++ b/src/Elasticsearch/Extension/SortExtension.php @@ -49,7 +49,6 @@ public function applyToCollection(array $requestBody, string $resourceClass, ?Op if ( $operation && null !== ($defaultOrder = $operation->getOrder()) - && \is_array($defaultOrder) ) { foreach ($defaultOrder as $property => $direction) { if (\is_int($property)) { diff --git a/src/Elasticsearch/Filter/AbstractFilter.php b/src/Elasticsearch/Filter/AbstractFilter.php index a989cd18339..a305a57e03b 100644 --- a/src/Elasticsearch/Filter/AbstractFilter.php +++ b/src/Elasticsearch/Filter/AbstractFilter.php @@ -19,8 +19,14 @@ use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; /** * Abstract class with helpers for easing the implementation of a filter. @@ -31,7 +37,9 @@ */ abstract class AbstractFilter implements FilterInterface { - use FieldDatatypeTrait { getNestedFieldPath as protected; } + use FieldDatatypeTrait { + getNestedFieldPath as protected; + } public function __construct(protected PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, protected ?NameConverterInterface $nameConverter = null, protected ?array $properties = null) { @@ -70,8 +78,108 @@ protected function hasProperty(string $resourceClass, string $property): bool * - is the decomposed given property an association? * - the resource class of the decomposed given property * - the property name of the decomposed given property + * + * @return array{0: ?Type, 1: ?bool, 2: ?class-string, 3: ?string} */ protected function getMetadata(string $resourceClass, string $property): array + { + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + return $this->getLegacyMetadata($resourceClass, $property); + } + + $noop = [null, null, null, null]; + + if (!$this->hasProperty($resourceClass, $property)) { + return $noop; + } + + $properties = explode('.', $property); + $totalProperties = \count($properties); + $currentResourceClass = $resourceClass; + $hasAssociation = false; + $currentProperty = null; + $type = null; + + foreach ($properties as $index => $currentProperty) { + try { + $propertyMetadata = $this->propertyMetadataFactory->create($currentResourceClass, $currentProperty); + } catch (PropertyNotFoundException) { + return $noop; + } + + // check each type before deciding if it's noop or not + // e.g: maybe the first type is noop, but the second is valid + $isNoop = false; + + ++$index; + + $type = $propertyMetadata->getNativeType(); + + if (null === $type) { + return $noop; + } + + foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) { + $builtinType = $t; + + while ($builtinType instanceof WrappingTypeInterface) { + $builtinType = $builtinType->getWrappedType(); + } + + if (!$builtinType instanceof ObjectType && !$t instanceof CollectionType) { + if ($totalProperties === $index) { + break 2; + } + + $isNoop = true; + + continue; + } + + if ($t instanceof CollectionType) { + $t = $t->getCollectionValueType(); + $builtinType = $t; + + while ($builtinType instanceof WrappingTypeInterface) { + $builtinType = $builtinType->getWrappedType(); + } + + if (!$builtinType instanceof ObjectType) { + if ($totalProperties === $index) { + break 2; + } + + $isNoop = true; + + continue; + } + } + + $className = $builtinType->getClassName(); + + if ($isResourceClass = $this->resourceClassResolver->isResourceClass($className)) { + $currentResourceClass = $className; + } elseif ($totalProperties !== $index) { + $isNoop = true; + + continue; + } + + $hasAssociation = $totalProperties === $index && $isResourceClass; + $isNoop = false; + + break; + } + } + + if ($isNoop) { + return $noop; + } + + return [$type, $hasAssociation, $currentResourceClass, $currentProperty]; + } + + protected function getLegacyMetadata(string $resourceClass, string $property): array { $noop = [null, null, null, null]; @@ -108,7 +216,7 @@ protected function getMetadata(string $resourceClass, string $property): array foreach ($types as $type) { $builtinType = $type->getBuiltinType(); - if (Type::BUILTIN_TYPE_OBJECT !== $builtinType && Type::BUILTIN_TYPE_ARRAY !== $builtinType) { + if (LegacyType::BUILTIN_TYPE_OBJECT !== $builtinType && LegacyType::BUILTIN_TYPE_ARRAY !== $builtinType) { if ($totalProperties === $index) { break 2; } @@ -124,7 +232,7 @@ protected function getMetadata(string $resourceClass, string $property): array continue; } - if (Type::BUILTIN_TYPE_ARRAY === $builtinType && Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) { + if (LegacyType::BUILTIN_TYPE_ARRAY === $builtinType && LegacyType::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) { if ($totalProperties === $index) { break 2; } diff --git a/src/Elasticsearch/Filter/AbstractSearchFilter.php b/src/Elasticsearch/Filter/AbstractSearchFilter.php index 318289dd697..a20fe911f97 100644 --- a/src/Elasticsearch/Filter/AbstractSearchFilter.php +++ b/src/Elasticsearch/Filter/AbstractSearchFilter.php @@ -21,8 +21,11 @@ use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Abstract class with helpers for easing the implementation of a search filter like a term filter or a match filter. @@ -109,27 +112,40 @@ public function getDescription(string $resourceClass): array */ abstract protected function getQuery(string $property, array $values, ?string $nestedPath): array; - /** - * Converts the given {@see Type} in PHP type. - */ - protected function getPhpType(Type $type): string + protected function getPhpType(LegacyType|Type $type): string { - switch ($builtinType = $type->getBuiltinType()) { - case Type::BUILTIN_TYPE_ARRAY: - case Type::BUILTIN_TYPE_INT: - case Type::BUILTIN_TYPE_FLOAT: - case Type::BUILTIN_TYPE_BOOL: - case Type::BUILTIN_TYPE_STRING: - return $builtinType; - case Type::BUILTIN_TYPE_OBJECT: - if (null !== ($className = $type->getClassName()) && is_a($className, \DateTimeInterface::class, true)) { - return \DateTimeInterface::class; - } + if ($type instanceof LegacyType) { + switch ($builtinType = $type->getBuiltinType()) { + case LegacyType::BUILTIN_TYPE_ARRAY: + case LegacyType::BUILTIN_TYPE_INT: + case LegacyType::BUILTIN_TYPE_FLOAT: + case LegacyType::BUILTIN_TYPE_BOOL: + case LegacyType::BUILTIN_TYPE_STRING: + return $builtinType; + case LegacyType::BUILTIN_TYPE_OBJECT: + if (null !== ($className = $type->getClassName()) && is_a($className, \DateTimeInterface::class, true)) { + return \DateTimeInterface::class; + } + + // no break + default: + return 'string'; + } + } + + if ($type->isIdentifiedBy(TypeIdentifier::ARRAY, TypeIdentifier::INT, TypeIdentifier::FLOAT, TypeIdentifier::BOOL, TypeIdentifier::STRING)) { + while ($type instanceof WrappingTypeInterface) { + $type = $type->getWrappedType(); + } + + return (string) $type; + } - // no break - default: - return 'string'; + if ($type->isIdentifiedBy(\DateTimeInterface::class)) { + return \DateTimeInterface::class; } + + return 'string'; } /** @@ -164,15 +180,26 @@ protected function getIdentifierValue(string $iri, string $property): mixed return $iri; } - /** - * Are the given values valid according to the given {@see Type}? - */ - protected function hasValidValues(array $values, Type $type): bool + protected function hasValidValues(array $values, LegacyType|Type $type): bool { + if ($type instanceof LegacyType) { + foreach ($values as $value) { + if ( + null !== $value + && LegacyType::BUILTIN_TYPE_INT === $type->getBuiltinType() + && false === filter_var($value, \FILTER_VALIDATE_INT) + ) { + return false; + } + } + + return true; + } + foreach ($values as $value) { if ( null !== $value - && Type::BUILTIN_TYPE_INT === $type->getBuiltinType() + && $type->isIdentifiedBy(TypeIdentifier::INT) && false === filter_var($value, \FILTER_VALIDATE_INT) ) { return false; diff --git a/src/Elasticsearch/Metadata/Document/DocumentMetadata.php b/src/Elasticsearch/Metadata/Document/DocumentMetadata.php deleted file mode 100644 index 7911865e99d..00000000000 --- a/src/Elasticsearch/Metadata/Document/DocumentMetadata.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document; - -/** - * Document metadata. - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html - * @deprecated - * - * @author Baptiste Meyer - */ -final class DocumentMetadata -{ - public const DEFAULT_TYPE = '_doc'; - - public function __construct(private ?string $index = null, private string $type = self::DEFAULT_TYPE) - { - } - - /** - * Gets a new instance with the given index. - */ - public function withIndex(string $index): self - { - $metadata = clone $this; - $metadata->index = $index; - - return $metadata; - } - - /** - * Gets the document index. - */ - public function getIndex(): ?string - { - return $this->index; - } - - /** - * Gets a new instance with the given type. - */ - public function withType(string $type): self - { - $metadata = clone $this; - $metadata->type = $type; - - return $metadata; - } - - /** - * Gets the document type. - */ - public function getType(): string - { - return $this->type; - } -} diff --git a/src/Elasticsearch/Metadata/Document/Factory/AttributeDocumentMetadataFactory.php b/src/Elasticsearch/Metadata/Document/Factory/AttributeDocumentMetadataFactory.php deleted file mode 100644 index 5475bc90b7c..00000000000 --- a/src/Elasticsearch/Metadata/Document/Factory/AttributeDocumentMetadataFactory.php +++ /dev/null @@ -1,74 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; - -/** - * Creates document's metadata using the attribute configuration. - * - * @deprecated - * - * @author Baptiste Meyer - */ -final class AttributeDocumentMetadataFactory implements DocumentMetadataFactoryInterface -{ - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly ?DocumentMetadataFactoryInterface $decorated = null) - { - } - - /** - * {@inheritdoc} - */ - public function create(string $resourceClass): DocumentMetadata - { - $documentMetadata = null; - - if ($this->decorated) { - try { - $documentMetadata = $this->decorated->create($resourceClass); - } catch (IndexNotFoundException) { - } - } - - $resourceMetadata = null; - - if (!$documentMetadata || null === $documentMetadata->getIndex()) { - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - - $index = $resourceMetadata->getOperation()->getExtraProperties()['elasticsearch_index'] ?? null; - - if (null !== $index) { - $documentMetadata = $documentMetadata ? $documentMetadata->withIndex($index) : new DocumentMetadata($index); - } - } - - if (!$documentMetadata || DocumentMetadata::DEFAULT_TYPE === $documentMetadata->getType()) { - $resourceMetadata ??= $this->resourceMetadataFactory->create($resourceClass); - $type = $resourceMetadata->getOperation()->getExtraProperties()['elasticsearch_type'] ?? null; - - if (null !== $type) { - $documentMetadata = $documentMetadata ? $documentMetadata->withType($type) : new DocumentMetadata(null, $type); - } - } - - if ($documentMetadata) { - return $documentMetadata; - } - - throw new IndexNotFoundException(\sprintf('No index associated with the "%s" resource class.', $resourceClass)); - } -} diff --git a/src/Elasticsearch/Metadata/Document/Factory/CachedDocumentMetadataFactory.php b/src/Elasticsearch/Metadata/Document/Factory/CachedDocumentMetadataFactory.php deleted file mode 100644 index bc78eab486d..00000000000 --- a/src/Elasticsearch/Metadata/Document/Factory/CachedDocumentMetadataFactory.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use Psr\Cache\CacheException; -use Psr\Cache\CacheItemPoolInterface; - -/** - * Caches document metadata. - * - * @deprecated - * - * @author Baptiste Meyer - */ -final class CachedDocumentMetadataFactory implements DocumentMetadataFactoryInterface -{ - private const CACHE_KEY_PREFIX = 'index_metadata'; - private array $localCache = []; - - public function __construct(private readonly CacheItemPoolInterface $cacheItemPool, private readonly DocumentMetadataFactoryInterface $decorated) - { - } - - /** - * {@inheritdoc} - */ - public function create(string $resourceClass): DocumentMetadata - { - if (isset($this->localCache[$resourceClass])) { - return $this->handleNotFound($this->localCache[$resourceClass], $resourceClass); - } - - try { - $cacheItem = $this->cacheItemPool->getItem(self::CACHE_KEY_PREFIX.md5($resourceClass)); - } catch (CacheException) { - return $this->handleNotFound($this->localCache[$resourceClass] = $this->decorated->create($resourceClass), $resourceClass); - } - - if ($cacheItem->isHit()) { - return $this->handleNotFound($this->localCache[$resourceClass] = $cacheItem->get(), $resourceClass); - } - - $documentMetadata = $this->decorated->create($resourceClass); - - $cacheItem->set($documentMetadata); - $this->cacheItemPool->save($cacheItem); - - return $this->handleNotFound($this->localCache[$resourceClass] = $documentMetadata, $resourceClass); - } - - /** - * @throws IndexNotFoundException - */ - private function handleNotFound(DocumentMetadata $documentMetadata, string $resourceClass): DocumentMetadata - { - if (null === $documentMetadata->getIndex()) { - throw new IndexNotFoundException(\sprintf('No index associated with the "%s" resource class.', $resourceClass)); - } - - return $documentMetadata; - } -} diff --git a/src/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php b/src/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php deleted file mode 100644 index a4f2d438571..00000000000 --- a/src/Elasticsearch/Metadata/Document/Factory/CatDocumentMetadataFactory.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Util\Inflector; -use Elastic\Elasticsearch\Exception\ClientResponseException; -use Elasticsearch\Client; -use Elasticsearch\Common\Exceptions\Missing404Exception; - -/** - * Creates document's metadata using indices from the cat APIs. - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-indices.html - * @deprecated - * - * @author Baptiste Meyer - */ -final class CatDocumentMetadataFactory implements DocumentMetadataFactoryInterface -{ - // @phpstan-ignore-next-line - public function __construct(private readonly Client $client, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly ?DocumentMetadataFactoryInterface $decorated = null) - { - } - - /** - * {@inheritdoc} - */ - public function create(string $resourceClass): DocumentMetadata - { - $documentMetadata = null; - - if ($this->decorated) { - try { - $documentMetadata = $this->decorated->create($resourceClass); - } catch (IndexNotFoundException) { - } - } - - if ($documentMetadata && null !== $documentMetadata->getIndex()) { - return $documentMetadata; - } - - $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); - $resourceShortName = $resourceMetadata->getOperation()->getShortName(); - - if (null === $resourceShortName) { - return $this->handleNotFound($documentMetadata, $resourceClass); - } - - $index = Inflector::tableize($resourceShortName); - - try { - // @phpstan-ignore-next-line - $this->client->cat()->indices(['index' => $index]); - // @phpstan-ignore-next-line - } catch (Missing404Exception|ClientResponseException) { - return $this->handleNotFound($documentMetadata, $resourceClass); - } - - return ($documentMetadata ?? new DocumentMetadata())->withIndex($index); - } - - /** - * @throws IndexNotFoundException - */ - private function handleNotFound(?DocumentMetadata $documentMetadata, string $resourceClass): DocumentMetadata - { - if ($documentMetadata) { - return $documentMetadata; - } - - throw new IndexNotFoundException(\sprintf('No index associated with the "%s" resource class.', $resourceClass)); - } -} diff --git a/src/Elasticsearch/Metadata/Document/Factory/ConfiguredDocumentMetadataFactory.php b/src/Elasticsearch/Metadata/Document/Factory/ConfiguredDocumentMetadataFactory.php deleted file mode 100644 index 5632b421a52..00000000000 --- a/src/Elasticsearch/Metadata/Document/Factory/ConfiguredDocumentMetadataFactory.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; - -/** - * Creates document's metadata using the mapping configuration. - * - * @deprecated - * - * @author Baptiste Meyer - */ -final class ConfiguredDocumentMetadataFactory implements DocumentMetadataFactoryInterface -{ - public function __construct(private readonly array $mapping, private readonly ?DocumentMetadataFactoryInterface $decorated = null) - { - } - - /** - * {@inheritdoc} - */ - public function create(string $resourceClass): DocumentMetadata - { - $documentMetadata = null; - - if ($this->decorated) { - try { - $documentMetadata = $this->decorated->create($resourceClass); - } catch (IndexNotFoundException) { - } - } - - if (null === $index = $this->mapping[$resourceClass] ?? null) { - if ($documentMetadata) { - return $documentMetadata; - } - - throw new IndexNotFoundException(\sprintf('No index associated with the "%s" resource class.', $resourceClass)); - } - - $documentMetadata ??= new DocumentMetadata(); - - if (isset($index['index'])) { - $documentMetadata = $documentMetadata->withIndex($index['index']); - } - - if (isset($index['type'])) { - $documentMetadata = $documentMetadata->withType($index['type']); - } - - return $documentMetadata; - } -} diff --git a/src/Elasticsearch/Metadata/Document/Factory/DocumentMetadataFactoryInterface.php b/src/Elasticsearch/Metadata/Document/Factory/DocumentMetadataFactoryInterface.php deleted file mode 100644 index 89f0da092da..00000000000 --- a/src/Elasticsearch/Metadata/Document/Factory/DocumentMetadataFactoryInterface.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; - -/** - * Creates a document metadata value object. - * - * @deprecated - * - * @author Baptiste Meyer - */ -interface DocumentMetadataFactoryInterface -{ - /** - * Creates document metadata. - * - * @throws IndexNotFoundException - */ - public function create(string $resourceClass): DocumentMetadata; -} diff --git a/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php b/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php index c42d8061d0c..f15819a1031 100644 --- a/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php +++ b/src/Elasticsearch/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactory.php @@ -17,17 +17,12 @@ use ApiPlatform\Elasticsearch\State\ItemProvider; use ApiPlatform\Elasticsearch\State\Options; use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Metadata\Util\Inflector; -use Elasticsearch\Client; -use Elasticsearch\Common\Exceptions\Missing404Exception; -use Elasticsearch\Common\Exceptions\NoNodesAvailableException; final class ElasticsearchProviderResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface { - public function __construct(private readonly ?Client $client, private readonly ResourceMetadataCollectionFactoryInterface $decorated, private readonly bool $triggerDeprecation = true) // @phpstan-ignore-line + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $decorated) { } @@ -47,18 +42,7 @@ public function create(string $resourceClass): ResourceMetadataCollection continue; } - if (null !== ($elasticsearch = $operation->getElasticsearch())) { - trigger_deprecation('api-platform/core', '3.1', \sprintf('The "elasticsearch" property is deprecated. Use a stateOptions: "%s" instead.', Options::class)); - } - - $hasElasticsearch = true === $elasticsearch || $operation->getStateOptions() instanceof Options; - - // Old behavior in ES < 8 - if ($this->client instanceof LegacyClient && $this->hasIndices($operation)) { // @phpstan-ignore-line - $hasElasticsearch = true; - } - - if (!$hasElasticsearch) { + if (!$operation->getStateOptions() instanceof Options) { continue; } @@ -76,18 +60,7 @@ public function create(string $resourceClass): ResourceMetadataCollection continue; } - if (null !== ($elasticsearch = $graphQlOperation->getElasticsearch())) { - trigger_deprecation('api-platform/core', '3.1', \sprintf('The "elasticsearch" property is deprecated. Use a stateOptions: "%s" instead.', Options::class)); - } - - $hasElasticsearch = true === $elasticsearch || $graphQlOperation->getStateOptions() instanceof Options; - - // Old behavior in ES < 8 - if ($this->client instanceof LegacyClient && $this->hasIndices($operation)) { // @phpstan-ignore-line - $hasElasticsearch = true; - } - - if (!$hasElasticsearch) { + if (!$graphQlOperation->getStateOptions() instanceof Options) { continue; } @@ -102,18 +75,4 @@ public function create(string $resourceClass): ResourceMetadataCollection return $resourceMetadataCollection; } - - private function hasIndices(Operation $operation): bool - { - $shortName = $operation->getShortName(); - $index = Inflector::tableize($shortName); - - try { - $this->client->cat()->indices(['index' => $index]); // @phpstan-ignore-line - - return true; - } catch (Missing404Exception|NoNodesAvailableException) { // @phpstan-ignore-line - return false; - } - } } diff --git a/src/Elasticsearch/Paginator.php b/src/Elasticsearch/Paginator.php index 2635be0b62d..2a1c6edc70e 100644 --- a/src/Elasticsearch/Paginator.php +++ b/src/Elasticsearch/Paginator.php @@ -95,7 +95,7 @@ public function getIterator(): \Traversable $denormalizationContext = array_merge([AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true], $this->denormalizationContext); foreach ($this->documents['hits']['hits'] ?? [] as $document) { - $cacheKey = isset($document['_index'], $document['_id']) ? md5("{$document['_index']}_{$document['_id']}") : null; + $cacheKey = isset($document['_index'], $document['_id']) ? hash('xxh3', "{$document['_index']}_{$document['_id']}") : null; if ($cacheKey && \array_key_exists($cacheKey, $this->cachedDenormalizedDocuments)) { $object = $this->cachedDenormalizedDocuments[$cacheKey]; diff --git a/src/Elasticsearch/README.md b/src/Elasticsearch/README.md index 354344479f6..67ad5bc7005 100644 --- a/src/Elasticsearch/README.md +++ b/src/Elasticsearch/README.md @@ -1,7 +1,12 @@ -# API Platform - Elasticsearch +# API Platform - Elasticsearch Support -Elasticsearch Support. - -## Resources +Integration for [Elasticsearch](https://www.elastic.co/elasticsearch) with the [API Platform](https://api-platform.com) framework. +[Documentation](https://api-platform.com/docs/core/elasticsearch/) +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/Elasticsearch/Serializer/ItemNormalizer.php b/src/Elasticsearch/Serializer/ItemNormalizer.php index 5bec2e647ca..01c5020a40e 100644 --- a/src/Elasticsearch/Serializer/ItemNormalizer.php +++ b/src/Elasticsearch/Serializer/ItemNormalizer.php @@ -13,12 +13,9 @@ namespace ApiPlatform\Elasticsearch\Serializer; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Exception\LogicException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -28,7 +25,7 @@ * * @experimental */ -final class ItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface, CacheableSupportsMethodInterface +final class ItemNormalizer implements NormalizerInterface, DenormalizerInterface, SerializerAwareInterface { public const FORMAT = 'elasticsearch'; @@ -36,27 +33,6 @@ public function __construct(private readonly NormalizerInterface $decorated) { } - /** - * @throws LogicException - */ - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - if (!$this->decorated instanceof BaseCacheableSupportsMethodInterface) { - throw new LogicException(\sprintf('The decorated normalizer must be an instance of "%s".', BaseCacheableSupportsMethodInterface::class)); - } - - return $this->decorated->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} * @@ -101,16 +77,11 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return DocumentNormalizer::FORMAT !== $format && $this->decorated->supportsNormalization($data, $format); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->decorated, 'getSupportedTypes')) { - return [ - DocumentNormalizer::FORMAT => null, - '*' => $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(), - ]; - } - return DocumentNormalizer::FORMAT !== $format ? $this->decorated->getSupportedTypes($format) : []; } diff --git a/src/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php b/src/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php index c55943d0054..248c0ad476e 100644 --- a/src/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php +++ b/src/Elasticsearch/Serializer/NameConverter/InnerFieldsNameConverter.php @@ -24,7 +24,7 @@ * * @author Baptiste Meyer */ -final class InnerFieldsNameConverter implements AdvancedNameConverterInterface +final class InnerFieldsNameConverter implements NameConverterInterface, AdvancedNameConverterInterface { public function __construct(private readonly NameConverterInterface $inner = new CamelCaseToSnakeCaseNameConverter()) { diff --git a/src/Elasticsearch/State/CollectionProvider.php b/src/Elasticsearch/State/CollectionProvider.php index 83e2e7adb58..df84a52e32d 100644 --- a/src/Elasticsearch/State/CollectionProvider.php +++ b/src/Elasticsearch/State/CollectionProvider.php @@ -14,9 +14,8 @@ namespace ApiPlatform\Elasticsearch\State; use ApiPlatform\Elasticsearch\Extension\RequestBodySearchCollectionExtensionInterface; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; use ApiPlatform\Elasticsearch\Paginator; +use ApiPlatform\Metadata\InflectorInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Util\Inflector; use ApiPlatform\State\ApiResource\Error; @@ -25,7 +24,8 @@ use Elastic\Elasticsearch\Client; use Elastic\Elasticsearch\Exception\ClientResponseException; use Elastic\Elasticsearch\Response\Elasticsearch; -use Elasticsearch\Client as LegacyClient; +use Elasticsearch\Client as V7Client; +use Elasticsearch\Common\Exceptions\Missing404Exception as V7Missing404Exception; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; /** @@ -39,8 +39,13 @@ final class CollectionProvider implements ProviderInterface /** * @param RequestBodySearchCollectionExtensionInterface[] $collectionExtensions */ - public function __construct(private readonly LegacyClient|Client $client, private readonly ?DocumentMetadataFactoryInterface $documentMetadataFactory = null, private readonly ?DenormalizerInterface $denormalizer = null, private readonly ?Pagination $pagination = null, private readonly iterable $collectionExtensions = []) // @phpstan-ignore-line - { + public function __construct( + private readonly V7Client|Client $client, // @phpstan-ignore-line + private readonly ?DenormalizerInterface $denormalizer = null, + private readonly ?Pagination $pagination = null, + private readonly iterable $collectionExtensions = [], + private readonly ?InflectorInterface $inflector = new Inflector(), + ) { } /** @@ -64,11 +69,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $options = $operation->getStateOptions() instanceof Options ? $operation->getStateOptions() : new Options(index: $this->getIndex($operation)); - // TODO: remove in 4.x - if ($this->documentMetadataFactory && $operation->getElasticsearch() && !$operation->getStateOptions()) { - $options = $this->convertDocumentMetadata($this->documentMetadataFactory->create($resourceClass)); - } - $params = [ 'index' => $options->getIndex() ?? $this->getIndex($operation), 'body' => $body, @@ -76,12 +76,14 @@ public function provide(Operation $operation, array $uriVariables = [], array $c try { $documents = $this->client->search($params); // @phpstan-ignore-line + } catch (V7Missing404Exception $e) { // @phpstan-ignore-line + throw new Error(status: $e->getCode(), detail: $e->getMessage(), title: $e->getMessage(), originalTrace: $e->getTrace()); // @phpstan-ignore-line } catch (ClientResponseException $e) { $response = $e->getResponse(); throw new Error(status: $response->getStatusCode(), detail: (string) $response->getBody(), title: $response->getReasonPhrase(), originalTrace: $e->getTrace()); } - if ($documents instanceof Elasticsearch) { + if (class_exists(Elasticsearch::class) && $documents instanceof Elasticsearch) { $documents = $documents->asArray(); } @@ -95,13 +97,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c ); } - private function convertDocumentMetadata(DocumentMetadata $documentMetadata): Options - { - return new Options($documentMetadata->getIndex(), $documentMetadata->getType()); - } - private function getIndex(Operation $operation): string { - return Inflector::tableize($operation->getShortName()); + return $this->inflector->tableize($operation->getShortName()); } } diff --git a/src/Elasticsearch/State/ItemProvider.php b/src/Elasticsearch/State/ItemProvider.php index cb749a9b772..49158ae547e 100644 --- a/src/Elasticsearch/State/ItemProvider.php +++ b/src/Elasticsearch/State/ItemProvider.php @@ -13,10 +13,8 @@ namespace ApiPlatform\Elasticsearch\State; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; use ApiPlatform\Elasticsearch\Serializer\DocumentNormalizer; -use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\InflectorInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Util\Inflector; use ApiPlatform\State\ApiResource\Error; @@ -24,8 +22,8 @@ use Elastic\Elasticsearch\Client; use Elastic\Elasticsearch\Exception\ClientResponseException; use Elastic\Elasticsearch\Response\Elasticsearch; -use Elasticsearch\Client as LegacyClient; -use Elasticsearch\Common\Exceptions\Missing404Exception; +use Elasticsearch\Client as V7Client; +use Elasticsearch\Common\Exceptions\Missing404Exception as V7Missing404Exception; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -37,8 +35,11 @@ */ final class ItemProvider implements ProviderInterface { - public function __construct(private readonly LegacyClient|Client $client, private readonly ?DocumentMetadataFactoryInterface $documentMetadataFactory = null, private readonly ?DenormalizerInterface $denormalizer = null) // @phpstan-ignore-line - { + public function __construct( + private readonly V7Client|Client $client, // @phpstan-ignore-line + private readonly ?DenormalizerInterface $denormalizer = null, + private readonly ?InflectorInterface $inflector = new Inflector(), + ) { } /** @@ -47,16 +48,9 @@ public function __construct(private readonly LegacyClient|Client $client, privat public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?object { $resourceClass = $operation->getClass(); - - $options = $operation->getStateOptions() instanceof Options ? $operation->getStateOptions() : new Options(index: $this->getIndex($operation)); - - // TODO: remove in 4.x - if ($this->documentMetadataFactory && $operation->getElasticsearch() && !$operation->getStateOptions()) { - $options = $this->convertDocumentMetadata($this->documentMetadataFactory->create($resourceClass)); - } - + $options = $operation->getStateOptions(); if (!$options instanceof Options) { - throw new RuntimeException(\sprintf('The "%s" provider was called without "%s".', self::class, Options::class)); + $options = new Options(index: $this->getIndex($operation)); } $params = [ @@ -66,7 +60,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c try { $document = $this->client->get($params); // @phpstan-ignore-line - } catch (Missing404Exception) { // @phpstan-ignore-line + } catch (V7Missing404Exception) { // @phpstan-ignore-line return null; } catch (ClientResponseException $e) { $response = $e->getResponse(); @@ -77,7 +71,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c throw new Error(status: $response->getStatusCode(), detail: (string) $response->getBody(), title: $response->getReasonPhrase(), originalTrace: $e->getTrace()); } - if ($document instanceof Elasticsearch) { + if (class_exists(Elasticsearch::class) && $document instanceof Elasticsearch) { $document = $document->asArray(); } @@ -89,13 +83,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c return $item; } - private function convertDocumentMetadata(DocumentMetadata $documentMetadata): Options - { - return new Options($documentMetadata->getIndex(), $documentMetadata->getType()); - } - private function getIndex(Operation $operation): string { - return Inflector::tableize($operation->getShortName()); + return $this->inflector->tableize($operation->getShortName()); } } diff --git a/src/Elasticsearch/State/Options.php b/src/Elasticsearch/State/Options.php index 8cd1d247374..fc82af387bc 100644 --- a/src/Elasticsearch/State/Options.php +++ b/src/Elasticsearch/State/Options.php @@ -19,10 +19,6 @@ class Options implements OptionsInterface { public function __construct( protected ?string $index = null, - /** - * @deprecated this parameter is not used anymore - */ - protected ?string $type = null, ) { } @@ -38,17 +34,4 @@ public function withIndex(?string $index): self return $self; } - - public function getType(): ?string - { - return $this->type; - } - - public function withType(?string $type): self - { - $self = clone $this; - $self->type = $type; - - return $self; - } } diff --git a/src/Elasticsearch/Tests/Extension/SortExtensionTest.php b/src/Elasticsearch/Tests/Extension/SortExtensionTest.php index f317a46e1a4..f2b67414b82 100644 --- a/src/Elasticsearch/Tests/Extension/SortExtensionTest.php +++ b/src/Elasticsearch/Tests/Extension/SortExtensionTest.php @@ -20,10 +20,11 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; class SortExtensionTest extends TestCase { @@ -53,12 +54,13 @@ public function testApplyToCollection(): void self::assertEquals(['sort' => [['name' => ['order' => 'asc']], ['bar' => ['order' => 'desc']]]], $sortExtension->applyToCollection([], Foo::class, (new GetCollection())->withOrder(['name', 'bar' => 'desc']))); } + #[IgnoreDeprecations] public function testApplyToCollectionWithNestedProperty(): void { - $fooType = new Type(Type::BUILTIN_TYPE_ARRAY, false, Foo::class, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class)); + $fooType = Type::list(Type::object(Foo::class)); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); diff --git a/src/Elasticsearch/Tests/Filter/MatchFilterTest.php b/src/Elasticsearch/Tests/Filter/MatchFilterTest.php index 82bf5623180..ebcc14aebcc 100644 --- a/src/Elasticsearch/Tests/Filter/MatchFilterTest.php +++ b/src/Elasticsearch/Tests/Filter/MatchFilterTest.php @@ -27,8 +27,8 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; class MatchFilterTest extends TestCase { @@ -55,8 +55,8 @@ public function testApply(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'name', 'bar']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $foo = new Foo(); $foo->setName('Xavier'); @@ -89,12 +89,12 @@ public function testApply(): void public function testApplyWithNestedArrayProperty(): void { - $fooType = new Type(Type::BUILTIN_TYPE_ARRAY, false, Foo::class, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class)); - $barType = new Type(Type::BUILTIN_TYPE_STRING); + $fooType = Type::list(Type::object(Foo::class)); + $barType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); @@ -121,12 +121,12 @@ public function testApplyWithNestedArrayProperty(): void public function testApplyWithNestedObjectProperty(): void { - $fooType = new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class); - $barType = new Type(Type::BUILTIN_TYPE_STRING); + $fooType = Type::object(Foo::class); + $barType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); @@ -156,7 +156,7 @@ public function testApplyWithInvalidFilters(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'bar']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -183,11 +183,11 @@ public function testGetDescription(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'name', 'bar', 'date', 'weird']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'date')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'weird')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_RESOURCE)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'date')->willReturn((new ApiProperty())->withNativeType(Type::object(\DateTimeImmutable::class)))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'weird')->willReturn((new ApiProperty())->withNativeType(Type::resource()))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(\DateTimeImmutable::class)->willReturn(false)->shouldBeCalled(); diff --git a/src/Elasticsearch/Tests/Filter/OrderFilterTest.php b/src/Elasticsearch/Tests/Filter/OrderFilterTest.php index 87f3186afb9..3aefbadeecb 100644 --- a/src/Elasticsearch/Tests/Filter/OrderFilterTest.php +++ b/src/Elasticsearch/Tests/Filter/OrderFilterTest.php @@ -24,8 +24,8 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; class OrderFilterTest extends TestCase { @@ -47,7 +47,7 @@ public function testConstruct(): void public function testApply(): void { $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); $nameConverterProphecy->normalize('name', Foo::class, null, Argument::type('array'))->willReturn('name')->shouldBeCalled(); @@ -69,12 +69,12 @@ public function testApply(): void public function testApplyWithNestedProperty(): void { - $fooType = new Type(Type::BUILTIN_TYPE_ARRAY, false, Foo::class, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class)); - $barType = new Type(Type::BUILTIN_TYPE_STRING); + $fooType = Type::list(Type::object(Foo::class)); + $barType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); @@ -114,7 +114,7 @@ public function testApplyWithInvalidOrderFilter(): void public function testApplyWithInvalidTypeAndInvalidDirection(): void { $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); @@ -137,7 +137,7 @@ public function testApplyWithInvalidTypeAndInvalidDirection(): void public function testDescription(): void { $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); $orderFilter = new OrderFilter( diff --git a/src/Elasticsearch/Tests/Filter/TermFilterTest.php b/src/Elasticsearch/Tests/Filter/TermFilterTest.php index a2836365d40..e7062bbbf84 100644 --- a/src/Elasticsearch/Tests/Filter/TermFilterTest.php +++ b/src/Elasticsearch/Tests/Filter/TermFilterTest.php @@ -27,8 +27,8 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; class TermFilterTest extends TestCase { @@ -55,8 +55,8 @@ public function testApply(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'name', 'bar']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $foo = new Foo(); $foo->setName('Xavier'); @@ -89,12 +89,12 @@ public function testApply(): void public function testApplyWithNestedArrayProperty(): void { - $fooType = new Type(Type::BUILTIN_TYPE_ARRAY, false, Foo::class, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class)); - $barType = new Type(Type::BUILTIN_TYPE_STRING); + $fooType = Type::list(Type::object(Foo::class)); + $barType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); @@ -121,12 +121,12 @@ public function testApplyWithNestedArrayProperty(): void public function testApplyWithNestedObjectProperty(): void { - $fooType = new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class); - $barType = new Type(Type::BUILTIN_TYPE_STRING); + $fooType = Type::object(Foo::class); + $barType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); @@ -156,7 +156,7 @@ public function testApplyWithInvalidFilters(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'bar']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); @@ -183,11 +183,11 @@ public function testGetDescription(): void $propertyNameCollectionFactoryProphecy->create(Foo::class)->willReturn(new PropertyNameCollection(['id', 'name', 'bar', 'date', 'weird']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'id')->willReturn((new ApiProperty())->withNativeType(Type::int()))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn(new ApiProperty())->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'date')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'weird')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_RESOURCE)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'date')->willReturn((new ApiProperty())->withNativeType(Type::object(\DateTimeImmutable::class)))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'weird')->willReturn((new ApiProperty())->withNativeType(Type::resource()))->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(\DateTimeImmutable::class)->willReturn(false)->shouldBeCalled(); diff --git a/src/Elasticsearch/Tests/Fixtures/Metadata/Get.php b/src/Elasticsearch/Tests/Fixtures/Metadata/Get.php new file mode 100644 index 00000000000..2f475372925 --- /dev/null +++ b/src/Elasticsearch/Tests/Fixtures/Metadata/Get.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Elasticsearch\Tests\Fixtures\Metadata; + +use ApiPlatform\Metadata\HttpOperation; + +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] +final class Get extends HttpOperation +{ + /** + * {@inheritdoc} + */ + public function __construct(...$args) + { + parent::__construct('GET', ...$args); + } +} diff --git a/src/Elasticsearch/Tests/Metadata/Document/DocumentMetadataTest.php b/src/Elasticsearch/Tests/Metadata/Document/DocumentMetadataTest.php deleted file mode 100644 index a7c304bb9d0..00000000000 --- a/src/Elasticsearch/Tests/Metadata/Document/DocumentMetadataTest.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\Metadata\Document; - -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use PHPUnit\Framework\TestCase; - -class DocumentMetadataTest extends TestCase -{ - public function testValueObject(): void - { - $documentMetadataOne = new DocumentMetadata('foo', 'bar'); - - self::assertSame('foo', $documentMetadataOne->getIndex()); - self::assertSame('bar', $documentMetadataOne->getType()); - - $documentMetadataTwo = $documentMetadataOne->withIndex('baz'); - - self::assertNotSame($documentMetadataTwo, $documentMetadataOne); - self::assertSame('baz', $documentMetadataTwo->getIndex()); - self::assertSame('bar', $documentMetadataTwo->getType()); - - $documentMetadataThree = $documentMetadataTwo->withType(DocumentMetadata::DEFAULT_TYPE); - - self::assertNotSame($documentMetadataThree, $documentMetadataOne); - self::assertNotSame($documentMetadataThree, $documentMetadataTwo); - self::assertSame('baz', $documentMetadataThree->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadataThree->getType()); - } -} diff --git a/src/Elasticsearch/Tests/Metadata/Document/Factory/AttributeDocumentMetadataFactoryTest.php b/src/Elasticsearch/Tests/Metadata/Document/Factory/AttributeDocumentMetadataFactoryTest.php deleted file mode 100644 index a39d31bee6f..00000000000 --- a/src/Elasticsearch/Tests/Metadata/Document/Factory/AttributeDocumentMetadataFactoryTest.php +++ /dev/null @@ -1,78 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\AttributeDocumentMetadataFactory; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Operations; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; - -class AttributeDocumentMetadataFactoryTest extends TestCase -{ - use ProphecyTrait; - - public function testConstruct(): void - { - self::assertInstanceOf( - DocumentMetadataFactoryInterface::class, - new AttributeDocumentMetadataFactory( - $this->prophesize(ResourceMetadataCollectionFactoryInterface::class)->reveal(), - $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal() - ) - ); - } - - public function testCreate(): void - { - $originalDocumentMetadata = new DocumentMetadata(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $resourceMetadata = new ResourceMetadataCollection(Foo::class, [(new ApiResource())->withOperations(new Operations([(new Get())->withExtraProperties(['elasticsearch_index' => 'foo', 'elasticsearch_type' => 'bar'])]))]); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled(); - - $documentMetadata = (new AttributeDocumentMetadataFactory($resourceMetadataFactoryProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertNotSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame('bar', $documentMetadata->getType()); - } - - public function testCreateWithNoParentDocumentMetadataAndNoAttributes(): void - { - $this->expectException(IndexNotFoundException::class); - $this->expectExceptionMessage(\sprintf('No index associated with the "%s" resource class.', Foo::class)); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willThrow(new IndexNotFoundException())->shouldBeCalled(); - - $resourceMetadata = new ResourceMetadataCollection(Foo::class, [(new ApiResource())->withOperations(new Operations([new Get()]))]); - - $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactoryProphecy->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled(); - - (new AttributeDocumentMetadataFactory($resourceMetadataFactoryProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - } -} diff --git a/src/Elasticsearch/Tests/Metadata/Document/Factory/CachedDocumentMetadataFactoryTest.php b/src/Elasticsearch/Tests/Metadata/Document/Factory/CachedDocumentMetadataFactoryTest.php deleted file mode 100644 index b5187c68cfd..00000000000 --- a/src/Elasticsearch/Tests/Metadata/Document/Factory/CachedDocumentMetadataFactoryTest.php +++ /dev/null @@ -1,159 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\CachedDocumentMetadataFactory; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Psr\Cache\CacheItemInterface; -use Psr\Cache\CacheItemPoolInterface; -use Symfony\Component\Cache\Exception\CacheException; - -class CachedDocumentMetadataFactoryTest extends TestCase -{ - use ProphecyTrait; - - public function testConstruct(): void - { - self::assertInstanceOf( - DocumentMetadataFactoryInterface::class, - new CachedDocumentMetadataFactory( - $this->prophesize(CacheItemPoolInterface::class)->reveal(), - $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal() - ) - ); - } - - public function testCreate(): void - { - $originalDocumentMetadata = new DocumentMetadata('foo'); - - $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); - $cacheItemProphecy->isHit()->willReturn(false)->shouldBeCalled(); - $cacheItemProphecy->set($originalDocumentMetadata)->willReturn($cacheItemProphecy)->shouldBeCalled(); - $cacheItemProphecy->get()->shouldNotBeCalled(); - - $cacheItemPoolProphecy = $this->prophesize(CacheItemPoolInterface::class); - $cacheItemPoolProphecy->getItem(Argument::type('string'))->willReturn($cacheItemProphecy)->shouldBeCalled(); - $cacheItemPoolProphecy->save($cacheItemProphecy)->willReturn(true)->shouldBeCalled(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $documentMetadata = (new CachedDocumentMetadataFactory($cacheItemPoolProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithLocalCache(): void - { - $originalDocumentMetadata = new DocumentMetadata('foo'); - - $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); - $cacheItemProphecy->isHit()->willReturn(false)->shouldBeCalledTimes(1); - $cacheItemProphecy->set($originalDocumentMetadata)->willReturn($cacheItemProphecy)->shouldBeCalledTimes(1); - $cacheItemProphecy->get()->shouldNotBeCalled(); - - $cacheItemPoolProphecy = $this->prophesize(CacheItemPoolInterface::class); - $cacheItemPoolProphecy->getItem(Argument::type('string'))->willReturn($cacheItemProphecy)->shouldBeCalledTimes(1); - $cacheItemPoolProphecy->save($cacheItemProphecy)->willReturn(true)->shouldBeCalledTimes(1); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalledTimes(1); - - $documentMetadataFactory = new CachedDocumentMetadataFactory($cacheItemPoolProphecy->reveal(), $decoratedProphecy->reveal()); - $documentMetadataFactory->create(Foo::class); - - $documentMetadata = $documentMetadataFactory->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithCacheException(): void - { - $originalDocumentMetadata = new DocumentMetadata('foo'); - - $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); - $cacheItemProphecy->isHit()->shouldNotBeCalled(); - $cacheItemProphecy->set(Argument::any())->shouldNotBeCalled(); - $cacheItemProphecy->get()->shouldNotBeCalled(); - - $cacheItemPoolProphecy = $this->prophesize(CacheItemPoolInterface::class); - $cacheItemPoolProphecy->getItem(Argument::type('string'))->willThrow(new CacheException())->shouldBeCalledTimes(1); - $cacheItemPoolProphecy->save(Argument::any())->shouldNotBeCalled(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $documentMetadata = (new CachedDocumentMetadataFactory($cacheItemPoolProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithCacheHit(): void - { - $originalDocumentMetadata = new DocumentMetadata('foo'); - - $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); - $cacheItemProphecy->isHit()->willReturn(true)->shouldBeCalled(); - $cacheItemProphecy->get()->willReturn($originalDocumentMetadata)->shouldBeCalled(); - $cacheItemProphecy->set(Argument::any())->shouldNotBeCalled(); - - $cacheItemPoolProphecy = $this->prophesize(CacheItemPoolInterface::class); - $cacheItemPoolProphecy->getItem(Argument::type('string'))->willReturn($cacheItemProphecy)->shouldBeCalled(); - $cacheItemPoolProphecy->save(Argument::any())->shouldNotBeCalled(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Argument::any())->shouldNotBeCalled(); - - $documentMetadata = (new CachedDocumentMetadataFactory($cacheItemPoolProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithIndexNotDefined(): void - { - $this->expectException(IndexNotFoundException::class); - $this->expectExceptionMessage(\sprintf('No index associated with the "%s" resource class.', Foo::class)); - - $originalDocumentMetadata = new DocumentMetadata(); - - $cacheItemProphecy = $this->prophesize(CacheItemInterface::class); - $cacheItemProphecy->isHit()->willReturn(false)->shouldBeCalled(); - $cacheItemProphecy->set($originalDocumentMetadata)->willReturn($cacheItemProphecy)->shouldBeCalled(); - $cacheItemProphecy->get()->shouldNotBeCalled(); - - $cacheItemPoolProphecy = $this->prophesize(CacheItemPoolInterface::class); - $cacheItemPoolProphecy->getItem(Argument::type('string'))->willReturn($cacheItemProphecy)->shouldBeCalled(); - $cacheItemPoolProphecy->save($cacheItemProphecy)->willReturn(true)->shouldBeCalled(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - (new CachedDocumentMetadataFactory($cacheItemPoolProphecy->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - } -} diff --git a/src/Elasticsearch/Tests/Metadata/Document/Factory/CatDocumentMetadataFactoryTest.php b/src/Elasticsearch/Tests/Metadata/Document/Factory/CatDocumentMetadataFactoryTest.php deleted file mode 100644 index 95b6028c324..00000000000 --- a/src/Elasticsearch/Tests/Metadata/Document/Factory/CatDocumentMetadataFactoryTest.php +++ /dev/null @@ -1,145 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\CatDocumentMetadataFactory; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Operations; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use Elasticsearch\Client; -use Elasticsearch\Common\Exceptions\Missing404Exception; -use Elasticsearch\Namespaces\CatNamespace; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; - -class CatDocumentMetadataFactoryTest extends TestCase -{ - use ProphecyTrait; - - protected function setUp(): void - { - if (interface_exists(\Elastic\Elasticsearch\ClientInterface::class)) { - $this->markTestSkipped('\Elastic\Elasticsearch\ClientInterface doesn\'t have cat method signature.'); - } - } - - public function testConstruct(): void - { - self::assertInstanceOf( - DocumentMetadataFactoryInterface::class, - new CatDocumentMetadataFactory( - $this->prophesize(Client::class)->reveal(), - $this->prophesize(ResourceMetadataCollectionFactoryInterface::class)->reveal(), - $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal() - ) - ); - } - - public function testCreate(): void - { - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willThrow(new IndexNotFoundException())->shouldBeCalled(); - - $resourceMetadata = new ResourceMetadataCollection(Foo::class, [(new ApiResource())->withOperations(new Operations([new Get(shortName: 'Foo')]))]); - - $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactory->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled(); - - $catNamespaceProphecy = $this->prophesize(CatNamespace::class); - $catNamespaceProphecy->indices(['index' => 'foo']) - ->willReturn([[ - 'health' => 'yellow', - 'status' => 'open', - 'index' => 'foo', - 'uuid' => '123456789abcdefghijklmn', - 'pri' => '5', - 'rep' => '1', - 'docs.count' => '42', - 'docs.deleted' => '0', - 'store.size' => '42kb', - 'pri.store.size' => '42kb', - ]]) - ->shouldBeCalled(); - - $clientProphecy = $this->prophesize(Client::class); - $clientProphecy->cat()->willReturn($catNamespaceProphecy)->shouldBeCalled(); - - $documentMetadata = (new CatDocumentMetadataFactory($clientProphecy->reveal(), $resourceMetadataFactory->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithIndexAlreadySet(): void - { - $originalDocumentMetadata = new DocumentMetadata('foo'); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $documentMetadata = (new CatDocumentMetadataFactory($this->prophesize(Client::class)->reveal(), $this->prophesize(ResourceMetadataCollectionFactoryInterface::class)->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertSame('foo', $documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithNoResourceShortName(): void - { - $originalDocumentMetadata = new DocumentMetadata(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $resourceMetadata = new ResourceMetadataCollection(Foo::class, [(new ApiResource())->withOperations(new Operations([new Get()]))]); - - $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactory->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled(); - - $documentMetadata = (new CatDocumentMetadataFactory($this->prophesize(Client::class)->reveal(), $resourceMetadataFactory->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $documentMetadata); - self::assertNull($documentMetadata->getIndex()); - self::assertSame(DocumentMetadata::DEFAULT_TYPE, $documentMetadata->getType()); - } - - public function testCreateWithIndexNotFound(): void - { - $this->expectException(IndexNotFoundException::class); - $this->expectExceptionMessage(\sprintf('No index associated with the "%s" resource class.', Foo::class)); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willThrow(new IndexNotFoundException())->shouldBeCalled(); - - $resourceMetadata = new ResourceMetadataCollection(Foo::class, [(new ApiResource())->withOperations(new Operations([new Get(shortName: 'Foo')]))]); - - $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataFactory->create(Foo::class)->willReturn($resourceMetadata)->shouldBeCalled(); - - $catNamespaceProphecy = $this->prophesize(CatNamespace::class); - // @phpstan-ignore-next-line - $catNamespaceProphecy->indices(['index' => 'foo'])->willThrow(new Missing404Exception())->shouldBeCalled(); - - $clientProphecy = $this->prophesize(Client::class); - $clientProphecy->cat()->willReturn($catNamespaceProphecy)->shouldBeCalled(); - - (new CatDocumentMetadataFactory($clientProphecy->reveal(), $resourceMetadataFactory->reveal(), $decoratedProphecy->reveal()))->create(Foo::class); - } -} diff --git a/src/Elasticsearch/Tests/Metadata/Document/Factory/ConfiguredDocumentMetadataFactoryTest.php b/src/Elasticsearch/Tests/Metadata/Document/Factory/ConfiguredDocumentMetadataFactoryTest.php deleted file mode 100644 index 772450d3a88..00000000000 --- a/src/Elasticsearch/Tests/Metadata/Document/Factory/ConfiguredDocumentMetadataFactoryTest.php +++ /dev/null @@ -1,72 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\Metadata\Document\Factory; - -use ApiPlatform\Elasticsearch\Exception\IndexNotFoundException; -use ApiPlatform\Elasticsearch\Metadata\Document\DocumentMetadata; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\ConfiguredDocumentMetadataFactory; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; - -class ConfiguredDocumentMetadataFactoryTest extends TestCase -{ - use ProphecyTrait; - - public function testConstruct(): void - { - self::assertInstanceOf( - DocumentMetadataFactoryInterface::class, - new ConfiguredDocumentMetadataFactory([], $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal()) - ); - } - - public function testCreate(): void - { - $originalDocumentMetadata = new DocumentMetadata(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $configuredDocumentMetadata = (new ConfiguredDocumentMetadataFactory([Foo::class => ['index' => 'foo', 'type' => 'bar']], $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertNotSame($originalDocumentMetadata, $configuredDocumentMetadata); - self::assertSame('foo', $configuredDocumentMetadata->getIndex()); - self::assertSame('bar', $configuredDocumentMetadata->getType()); - } - - public function testCreateWithEmptyMapping(): void - { - $originalDocumentMetadata = new DocumentMetadata(); - - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willReturn($originalDocumentMetadata)->shouldBeCalled(); - - $configuredDocumentMetadata = (new ConfiguredDocumentMetadataFactory([], $decoratedProphecy->reveal()))->create(Foo::class); - - self::assertSame($originalDocumentMetadata, $configuredDocumentMetadata); - } - - public function testCreateWithEmptyMappingAndNoParentDocumentMetadata(): void - { - $decoratedProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - $decoratedProphecy->create(Foo::class)->willThrow(new IndexNotFoundException())->shouldBeCalled(); - - $this->expectException(IndexNotFoundException::class); - $this->expectExceptionMessage(\sprintf('No index associated with the "%s" resource class.', Foo::class)); - - (new ConfiguredDocumentMetadataFactory([], $decoratedProphecy->reveal()))->create(Foo::class); - } -} diff --git a/src/Elasticsearch/Tests/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactoryTest.php b/src/Elasticsearch/Tests/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactoryTest.php index 4a7a22b579b..6d4f59f1695 100644 --- a/src/Elasticsearch/Tests/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactoryTest.php +++ b/src/Elasticsearch/Tests/Metadata/Resource/Factory/ElasticsearchProviderResourceMetadataCollectionFactoryTest.php @@ -14,22 +14,12 @@ namespace ApiPlatform\Elasticsearch\Tests\Metadata\Resource\Factory; use ApiPlatform\Elasticsearch\Metadata\Resource\Factory\ElasticsearchProviderResourceMetadataCollectionFactory; -use ApiPlatform\Elasticsearch\State\Options; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Metadata\Tests\Fixtures\Metadata\Get; -use Elasticsearch\Client as LegacyClient; -use Elasticsearch\Common\Exceptions\Missing404Exception; -use Elasticsearch\Namespaces\CatNamespace; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; class ElasticsearchProviderResourceMetadataCollectionFactoryTest extends TestCase { - use ExpectDeprecationTrait; use ProphecyTrait; public function testConstruct(): void @@ -37,69 +27,8 @@ public function testConstruct(): void self::assertInstanceOf( ResourceMetadataCollectionFactoryInterface::class, new ElasticsearchProviderResourceMetadataCollectionFactory( - class_exists(LegacyClient::class) ? $this->prophesize(LegacyClient::class)->reveal() : null, $this->prophesize(ResourceMetadataCollectionFactoryInterface::class)->reveal() ) ); } - - /** - * @dataProvider elasticsearchProvider - */ - public function testCreate(?bool $elasticsearchFlag, int $expectedCatCallCount, ?bool $expectedResult): void - { - if (interface_exists(\Elastic\Elasticsearch\ClientInterface::class)) { - $this->markTestSkipped('\Elastic\Elasticsearch\ClientInterface doesn\'t have cat method signature.'); - } - - if (null !== $elasticsearchFlag) { - $solution = $elasticsearchFlag - ? \sprintf('Pass an instance of %s to $stateOptions instead', Options::class) - : 'You will have to remove it when upgrading to v4'; - $this->expectDeprecation(\sprintf('Since api-platform/core 3.1: Setting "elasticsearch" in Operation is deprecated. %s', $solution)); - } - $get = (new Get(elasticsearch: $elasticsearchFlag, shortName: 'Foo')); - $resource = (new ApiResource(operations: ['foo_get' => $get])); - $metadata = new ResourceMetadataCollection(Foo::class, [$resource]); - - $decorated = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $decorated->create(Foo::class)->willReturn($metadata)->shouldBeCalled(); - - // @phpstan-ignore-next-line - $catNamespace = $this->prophesize(CatNamespace::class); - if ($elasticsearchFlag) { - $catNamespace->indices(['index' => 'foo'])->willReturn([[ - 'health' => 'yellow', - 'status' => 'open', - 'index' => 'foo', - 'uuid' => '123456789abcdefghijklmn', - 'pri' => '5', - 'rep' => '1', - 'docs.count' => '42', - 'docs.deleted' => '0', - 'store.size' => '42kb', - 'pri.store.size' => '42kb', - ]]); - } else { - // @phpstan-ignore-next-line - $catNamespace->indices(['index' => 'foo'])->willThrow(new Missing404Exception()); - } - - // @phpstan-ignore-next-line - $client = $this->prophesize(LegacyClient::class); - $client->cat()->willReturn($catNamespace)->shouldBeCalledTimes($expectedCatCallCount); - - $resourceMetadataFactory = new ElasticsearchProviderResourceMetadataCollectionFactory($client->reveal(), $decorated->reveal(), false); - $elasticsearchResult = $resourceMetadataFactory->create(Foo::class)->getOperation('foo_get')->getElasticsearch(); - self::assertEquals($expectedResult, $elasticsearchResult); - } - - public static function elasticsearchProvider(): array - { - return [ - 'elasticsearch: false' => [false, 0, false], - 'elasticsearch: null' => [null, 1, false], - 'elasticsearch: true' => [true, 1, true], - ]; - } } diff --git a/src/Elasticsearch/Tests/Serializer/ItemNormalizerTest.php b/src/Elasticsearch/Tests/Serializer/ItemNormalizerTest.php index e0994c9557a..7819eb3951c 100644 --- a/src/Elasticsearch/Tests/Serializer/ItemNormalizerTest.php +++ b/src/Elasticsearch/Tests/Serializer/ItemNormalizerTest.php @@ -19,10 +19,8 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Serializer\Exception\LogicException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; use Symfony\Component\Serializer\SerializerAwareInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -40,10 +38,6 @@ protected function setUp(): void ->willImplement(DenormalizerInterface::class) ->willImplement(SerializerAwareInterface::class); - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->normalizerProphecy->willImplement(CacheableSupportsMethodInterface::class); - } - $this->itemNormalizer = new ItemNormalizer($this->normalizerProphecy->reveal()); } @@ -54,20 +48,6 @@ public function testConstruct(): void self::assertInstanceOf(SerializerAwareInterface::class, $this->itemNormalizer); } - /** - * @group legacy - */ - public function testHasCacheableSupportsMethod(): void - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - $this->markTestSkipped('Symfony Serializer >= 6.3'); - } - - $this->normalizerProphecy->hasCacheableSupportsMethod()->willReturn(true)->shouldBeCalledOnce(); - - self::assertTrue($this->itemNormalizer->hasCacheableSupportsMethod()); - } - public function testDenormalize(): void { $this->normalizerProphecy->denormalize('foo', 'string', 'json', ['groups' => 'foo'])->willReturn('foo')->shouldBeCalledOnce(); @@ -107,17 +87,6 @@ public function testSetSerializer(): void $this->itemNormalizer->setSerializer($serializer); } - /** - * @group legacy - */ - public function testHasCacheableSupportsMethodWithDecoratedNormalizerNotAnInstanceOfCacheableSupportsMethodInterface(): void - { - $this->expectException(LogicException::class); - $this->expectExceptionMessage(\sprintf('The decorated normalizer must be an instance of "%s".', CacheableSupportsMethodInterface::class)); - - (new ItemNormalizer($this->prophesize(NormalizerInterface::class)->reveal()))->hasCacheableSupportsMethod(); - } - public function testDenormalizeWithDecoratedNormalizerNotAnInstanceOfDenormalizerInterface(): void { $this->expectException(LogicException::class); @@ -144,10 +113,6 @@ public function testSetSerializerWithDecoratedNormalizerNotAnInstanceOfSerialize public function testGetSupportedTypes(): void { - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->markTestSkipped('Symfony Serializer < 6.3'); - } - // TODO: use prophecy when getSupportedTypes() will be added to the interface $this->itemNormalizer = new ItemNormalizer(new class implements NormalizerInterface { public function normalize(mixed $object, ?string $format = null, array $context = []): \ArrayObject|array|string|int|float|bool|null diff --git a/src/Elasticsearch/Tests/State/CollectionProviderTest.php b/src/Elasticsearch/Tests/State/CollectionProviderTest.php deleted file mode 100644 index 6eec78c033b..00000000000 --- a/src/Elasticsearch/Tests/State/CollectionProviderTest.php +++ /dev/null @@ -1,148 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\State; - -use ApiPlatform\Elasticsearch\Extension\RequestBodySearchCollectionExtensionInterface; -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Paginator; -use ApiPlatform\Elasticsearch\State\CollectionProvider; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\State\Pagination\Pagination; -use Elastic\Elasticsearch\ClientInterface; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * @author Baptiste Meyer - * @author Vincent Chalamon - */ -final class CollectionProviderTest extends TestCase -{ - use ProphecyTrait; - - public function testConstruct(): void - { - if (interface_exists(ClientInterface::class)) { - $this->markTestSkipped('Can not test using Elasticsearch 8.'); - } - - self::assertInstanceOf( - CollectionProvider::class, - new CollectionProvider( - $this->prophesize(\Elasticsearch\Client::class)->reveal(), - $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal(), - $this->prophesize(DenormalizerInterface::class)->reveal(), - new Pagination() - ) - ); - } - - public function testGetCollection(): void - { - if (interface_exists(ClientInterface::class)) { - $this->markTestSkipped('Can not test using Elasticsearch 8.'); - } - - $context = [ - 'groups' => ['custom'], - ]; - - $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - - $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactoryProphecy->create(Foo::class)->willReturn(new ResourceMetadataCollection(Foo::class)); - - $documents = [ - 'took' => 15, - 'time_out' => false, - '_shards' => [ - 'total' => 5, - 'successful' => 5, - 'skipped' => 0, - 'failed' => 0, - ], - 'hits' => [ - 'total' => 4, - 'max_score' => 1, - 'hits' => [ - [ - '_index' => 'foo', - '_type' => '_doc', - '_id' => '1', - '_score' => 1, - '_source' => [ - 'id' => 1, - 'name' => 'Kilian', - 'bar' => 'Jornet', - ], - ], - [ - '_index' => 'foo', - '_type' => '_doc', - '_id' => '2', - '_score' => 1, - '_source' => [ - 'id' => 2, - 'name' => 'François', - 'bar' => 'D\'Haene', - ], - ], - ], - ], - ]; - - $clientProphecy = $this->prophesize(\Elasticsearch\Client::class); - $clientProphecy - ->search( - Argument::allOf( - Argument::withEntry('index', 'foo'), - Argument::withEntry('body', Argument::allOf( - Argument::withEntry('size', 2), - Argument::withEntry('from', 0), - Argument::withEntry('query', Argument::allOf( - Argument::withEntry('match_all', Argument::type(\stdClass::class)), - Argument::size(1) - )), - Argument::size(3) - )), - Argument::size(2) - ) - ) - ->willReturn($documents) - ->shouldBeCalled(); - - $operation = (new Get())->withName('get')->withClass(Foo::class)->withShortName('foo'); - - $requestBodySearchCollectionExtensionProphecy = $this->prophesize(RequestBodySearchCollectionExtensionInterface::class); - $requestBodySearchCollectionExtensionProphecy->applyToCollection([], Foo::class, $operation, $context)->willReturn([])->shouldBeCalled(); - - $provider = new CollectionProvider( - $clientProphecy->reveal(), - $documentMetadataFactoryProphecy->reveal(), - $denormalizer = $this->prophesize(DenormalizerInterface::class)->reveal(), - new Pagination(['items_per_page' => 2]), - [$requestBodySearchCollectionExtensionProphecy->reveal()] - ); - - self::assertEquals( - new Paginator($denormalizer, $documents, Foo::class, 2, 0, $context), - $provider->provide($operation, [], $context) - ); - } -} diff --git a/src/Elasticsearch/Tests/State/ItemProviderTest.php b/src/Elasticsearch/Tests/State/ItemProviderTest.php deleted file mode 100644 index 0a260acef4b..00000000000 --- a/src/Elasticsearch/Tests/State/ItemProviderTest.php +++ /dev/null @@ -1,108 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Elasticsearch\Tests\State; - -use ApiPlatform\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; -use ApiPlatform\Elasticsearch\Serializer\DocumentNormalizer; -use ApiPlatform\Elasticsearch\State\ItemProvider; -use ApiPlatform\Elasticsearch\Tests\Fixtures\Foo; -use ApiPlatform\Metadata\Get; -use Elastic\Elasticsearch\ClientInterface; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * @author Baptiste Meyer - * @author Vincent Chalamon - */ -final class ItemProviderTest extends TestCase -{ - use ProphecyTrait; - - public function testConstruct(): void - { - if (interface_exists(ClientInterface::class)) { - $this->markTestSkipped('Can not test using Elasticsearch 8.'); - } - - self::assertInstanceOf( - ItemProvider::class, - new ItemProvider( - $this->prophesize(\Elasticsearch\Client::class)->reveal(), - $this->prophesize(DocumentMetadataFactoryInterface::class)->reveal(), - $this->prophesize(DenormalizerInterface::class)->reveal() - ) - ); - } - - public function testGetItem(): void - { - if (interface_exists(ClientInterface::class)) { - $this->markTestSkipped('Can not test using Elasticsearch 8.'); - } - - $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - - $document = [ - '_index' => 'foo', - '_type' => '_doc', - '_id' => '1', - 'found' => true, - '_source' => [ - 'id' => 1, - 'name' => 'Rossinière', - 'bar' => 'erèinissor', - ], - ]; - - $foo = new Foo(); - $foo->setName('Rossinière'); - $foo->setBar('erèinissor'); - - $clientProphecy = $this->prophesize(\Elasticsearch\Client::class); - $clientProphecy->get(['index' => 'foo', 'id' => '1'])->willReturn($document)->shouldBeCalled(); - - $denormalizerProphecy = $this->prophesize(DenormalizerInterface::class); - $denormalizerProphecy->denormalize($document, Foo::class, DocumentNormalizer::FORMAT, [AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => true])->willReturn($foo)->shouldBeCalled(); - - $itemDataProvider = new ItemProvider($clientProphecy->reveal(), $documentMetadataFactoryProphecy->reveal(), $denormalizerProphecy->reveal()); - - self::assertSame($foo, $itemDataProvider->provide((new Get())->withClass(Foo::class)->withShortName('foo'), ['id' => 1])); - } - - public function testGetInexistantItem(): void - { - if (interface_exists(ClientInterface::class)) { - $this->markTestSkipped('Can not test using Elasticsearch 8.'); - } - - $documentMetadataFactoryProphecy = $this->prophesize(DocumentMetadataFactoryInterface::class); - - $clientClass = class_exists(\Elasticsearch\Client::class) ? \Elasticsearch\Client::class : ClientInterface::class; - - $clientProphecy = $this->prophesize($clientClass); - $clientProphecy->get(['index' => 'foo', 'id' => '404'])->willReturn([ - '_index' => 'foo', - '_type' => '_doc', - '_id' => '404', - 'found' => false, - ])->shouldBeCalled(); - - $itemDataProvider = new ItemProvider($clientProphecy->reveal(), $documentMetadataFactoryProphecy->reveal(), $this->prophesize(DenormalizerInterface::class)->reveal()); - - self::assertNull($itemDataProvider->provide((new Get())->withClass(Foo::class)->withShortName('foo'), ['id' => 404])); - } -} diff --git a/src/Elasticsearch/Tests/Util/FieldDatatypeTraitTest.php b/src/Elasticsearch/Tests/Util/FieldDatatypeTraitTest.php index cebb490d217..fec6ba90695 100644 --- a/src/Elasticsearch/Tests/Util/FieldDatatypeTraitTest.php +++ b/src/Elasticsearch/Tests/Util/FieldDatatypeTraitTest.php @@ -21,7 +21,7 @@ use ApiPlatform\Metadata\ResourceClassResolverInterface; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\TypeInfo\Type; class FieldDatatypeTraitTest extends TestCase { @@ -71,7 +71,7 @@ public function testGetNestedFieldPathWithPropertyWithoutType(): void public function testGetNestedFieldPathWithInvalidCollectionType(): void { $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalled(); $fieldDatatype = self::createFieldDatatypeInstance($propertyMetadataFactoryProphecy->reveal(), $this->prophesize(ResourceClassResolverInterface::class)->reveal()); @@ -90,14 +90,14 @@ public function testIsNestedField(): void private function getValidFieldDatatype() { - $fooType = new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class); - $barType = new Type(Type::BUILTIN_TYPE_ARRAY, false, Foo::class, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, Foo::class)); - $bazType = new Type(Type::BUILTIN_TYPE_STRING, false, Foo::class); + $fooType = Type::object(Foo::class); + $barType = Type::list(Type::object(Foo::class)); + $bazType = Type::string(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withBuiltinTypes([$fooType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withBuiltinTypes([$barType]))->shouldBeCalled(); - $propertyMetadataFactoryProphecy->create(Foo::class, 'baz')->willReturn((new ApiProperty())->withBuiltinTypes([$bazType])); + $propertyMetadataFactoryProphecy->create(Foo::class, 'foo')->willReturn((new ApiProperty())->withNativeType($fooType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'bar')->willReturn((new ApiProperty())->withNativeType($barType))->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Foo::class, 'baz')->willReturn((new ApiProperty())->withNativeType($bazType)); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Foo::class)->willReturn(true)->shouldBeCalled(); diff --git a/src/Elasticsearch/Util/FieldDatatypeTrait.php b/src/Elasticsearch/Util/FieldDatatypeTrait.php index 6defd345353..25a0fe81bc8 100644 --- a/src/Elasticsearch/Util/FieldDatatypeTrait.php +++ b/src/Elasticsearch/Util/FieldDatatypeTrait.php @@ -16,7 +16,11 @@ use ApiPlatform\Metadata\Exception\PropertyNotFoundException; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; -use Symfony\Component\PropertyInfo\Type; +use ApiPlatform\Metadata\Util\TypeHelper; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\ObjectType; /** * Field datatypes helpers. @@ -64,29 +68,58 @@ private function getNestedFieldPath(string $resourceClass, string $property): ?s return null; } - $types = $propertyMetadata->getBuiltinTypes() ?? []; + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $types = $propertyMetadata->getBuiltinTypes() ?? []; + + foreach ($types as $type) { + if ( + LegacyType::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() + && null !== ($nextResourceClass = $type->getClassName()) + && $this->resourceClassResolver->isResourceClass($nextResourceClass) + ) { + $nestedPath = $this->getNestedFieldPath($nextResourceClass, implode('.', $properties)); + + return null === $nestedPath ? $nestedPath : "$currentProperty.$nestedPath"; + } + + if ( + null !== ($type = $type->getCollectionValueTypes()[0] ?? null) + && LegacyType::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() + && null !== ($className = $type->getClassName()) + && $this->resourceClassResolver->isResourceClass($className) + ) { + $nestedPath = $this->getNestedFieldPath($className, implode('.', $properties)); + + return null === $nestedPath ? $currentProperty : "$currentProperty.$nestedPath"; + } + } - foreach ($types as $type) { - if ( - Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() - && null !== ($nextResourceClass = $type->getClassName()) - && $this->resourceClassResolver->isResourceClass($nextResourceClass) - ) { - $nestedPath = $this->getNestedFieldPath($nextResourceClass, implode('.', $properties)); + return null; + } - return null === $nestedPath ? $nestedPath : "$currentProperty.$nestedPath"; - } + $type = $propertyMetadata->getNativeType(); - if ( - null !== ($type = $type->getCollectionValueTypes()[0] ?? null) - && Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() - && null !== ($className = $type->getClassName()) - && $this->resourceClassResolver->isResourceClass($className) - ) { - $nestedPath = $this->getNestedFieldPath($className, implode('.', $properties)); + if (null === $type) { + return null; + } - return null === $nestedPath ? $currentProperty : "$currentProperty.$nestedPath"; - } + /** @var class-string|null $className */ + $className = null; + + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); + }; + + if ($type->isSatisfiedBy($typeIsResourceClass)) { + $nestedPath = $this->getNestedFieldPath($className, implode('.', $properties)); + + return null === $nestedPath ? $nestedPath : "$currentProperty.$nestedPath"; + } + + if (TypeHelper::getCollectionValueType($type)?->isSatisfiedBy($typeIsResourceClass)) { + $nestedPath = $this->getNestedFieldPath($className, implode('.', $properties)); + + return null === $nestedPath ? $currentProperty : "$currentProperty.$nestedPath"; } return null; diff --git a/src/Elasticsearch/composer.json b/src/Elasticsearch/composer.json index f7289857dc7..ac5cc4be2aa 100644 --- a/src/Elasticsearch/composer.json +++ b/src/Elasticsearch/composer.json @@ -3,8 +3,10 @@ "description": "API Platform Elasticsearch bridge", "type": "library", "keywords": [ - "Filter", - "Elasticsearch", + "REST", + "API", + "filter", + "elasticsearch", "search" ], "homepage": "https://api-platform.com/", @@ -21,22 +23,22 @@ } ], "require": { - "php": ">=8.1", - "api-platform/metadata": "*@dev || ^3.1", - "api-platform/serializer": "*@dev || ^3.1", - "api-platform/state": "*@dev || ^3.1", - "elasticsearch/elasticsearch": "^8.9", + "php": ">=8.2", + "api-platform/metadata": "^4.2@beta", + "api-platform/serializer": "^4.1.11", + "api-platform/state": "^4.1.11", + "elasticsearch/elasticsearch": "^7.17 || ^8.4", "symfony/cache": "^6.4 || ^7.0", "symfony/console": "^6.4 || ^7.0", "symfony/property-access": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.1", "symfony/serializer": "^6.4 || ^7.0", + "symfony/type-info": "^7.3", "symfony/uid": "^6.4 || ^7.0" }, "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", - "sebastian/comparator": "<5.0" + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "11.5.x-dev" }, "autoload": { "psr-4": { @@ -59,13 +61,26 @@ }, "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" }, "symfony": { - "require": "^6.4" + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" } }, "scripts": { "test": "./vendor/bin/phpunit" - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] } diff --git a/src/Elasticsearch/phpunit.baseline.xml b/src/Elasticsearch/phpunit.baseline.xml new file mode 100644 index 00000000000..e3ef6196399 --- /dev/null +++ b/src/Elasticsearch/phpunit.baseline.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Elasticsearch/phpunit.xml.dist b/src/Elasticsearch/phpunit.xml.dist index 0b1b38b4a5b..40d546c5064 100644 --- a/src/Elasticsearch/phpunit.xml.dist +++ b/src/Elasticsearch/phpunit.xml.dist @@ -1,31 +1,23 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + trigger_deprecation + + + ./ + + + ./Tests + ./vendor + + - diff --git a/src/Exception/DeserializationException.php b/src/Exception/DeserializationException.php deleted file mode 100644 index 37dfcb55772..00000000000 --- a/src/Exception/DeserializationException.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface; - -/** - * Deserialization exception. - * - * @author Samuel ROZE - * @author Kévin Dunglas - */ -class DeserializationException extends \Exception implements ExceptionInterface, SerializerExceptionInterface -{ -} diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php deleted file mode 100644 index 99fcdca6992..00000000000 --- a/src/Exception/ExceptionInterface.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Base exception interface. - * - * @author Kévin Dunglas - */ -interface ExceptionInterface extends \Throwable -{ -} diff --git a/src/Exception/FilterValidationException.php b/src/Exception/FilterValidationException.php deleted file mode 100644 index e5008b430c0..00000000000 --- a/src/Exception/FilterValidationException.php +++ /dev/null @@ -1,36 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -use ApiPlatform\ParameterValidator\Exception\ValidationExceptionInterface; - -/** - * Filter validation exception. - * - * @author Julien DENIAU - * - * @deprecated use \ApiPlatform\ParameterValidator\Exception\ValidationException instead - */ -final class FilterValidationException extends \Exception implements ValidationExceptionInterface, ExceptionInterface, \Stringable -{ - public function __construct(private readonly array $constraintViolationList, string $message = '', int $code = 0, ?\Exception $previous = null) - { - parent::__construct($message ?: $this->__toString(), $code, $previous); - } - - public function __toString(): string - { - return implode("\n", $this->constraintViolationList); - } -} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php deleted file mode 100644 index bddf0a57112..00000000000 --- a/src/Exception/InvalidArgumentException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Invalid argument exception. - * - * @author Kévin Dunglas - */ -class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface -{ -} diff --git a/src/Exception/InvalidIdentifierException.php b/src/Exception/InvalidIdentifierException.php deleted file mode 100644 index d77354ae30a..00000000000 --- a/src/Exception/InvalidIdentifierException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Identifier is not valid exception. - * - * @author Antoine Bluchet - */ -class InvalidIdentifierException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/InvalidResourceException.php b/src/Exception/InvalidResourceException.php deleted file mode 100644 index a69543355d7..00000000000 --- a/src/Exception/InvalidResourceException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Invalid resource exception. - * - * @author Paul Le Corre - */ -class InvalidResourceException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/InvalidUriVariableException.php b/src/Exception/InvalidUriVariableException.php deleted file mode 100644 index 5aedb5839a7..00000000000 --- a/src/Exception/InvalidUriVariableException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Identifier is not valid exception. - * - * @author Antoine Bluchet - */ -class InvalidUriVariableException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/InvalidValueException.php b/src/Exception/InvalidValueException.php deleted file mode 100644 index 96b023fdbae..00000000000 --- a/src/Exception/InvalidValueException.php +++ /dev/null @@ -1,18 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -class InvalidValueException extends InvalidArgumentException -{ -} diff --git a/src/Exception/ItemNotFoundException.php b/src/Exception/ItemNotFoundException.php deleted file mode 100644 index a66f39965d8..00000000000 --- a/src/Exception/ItemNotFoundException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Item not found exception. - * - * @author Amrouche Hamza - */ -class ItemNotFoundException extends InvalidArgumentException -{ -} diff --git a/src/Exception/OperationNotFoundException.php b/src/Exception/OperationNotFoundException.php deleted file mode 100644 index a112ef15da5..00000000000 --- a/src/Exception/OperationNotFoundException.php +++ /dev/null @@ -1,21 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Operation not found exception. - */ -class OperationNotFoundException extends \InvalidArgumentException implements ExceptionInterface -{ -} diff --git a/src/Exception/PropertyNotFoundException.php b/src/Exception/PropertyNotFoundException.php deleted file mode 100644 index 44df7ee03ec..00000000000 --- a/src/Exception/PropertyNotFoundException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Property not found exception. - * - * @author Kévin Dunglas - */ -class PropertyNotFoundException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/ResourceClassNotFoundException.php b/src/Exception/ResourceClassNotFoundException.php deleted file mode 100644 index 0a0f0eaa9c4..00000000000 --- a/src/Exception/ResourceClassNotFoundException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Resource class not found exception. - * - * @author Kévin Dunglas - */ -class ResourceClassNotFoundException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/ResourceClassNotSupportedException.php b/src/Exception/ResourceClassNotSupportedException.php deleted file mode 100644 index 9d291d1ae09..00000000000 --- a/src/Exception/ResourceClassNotSupportedException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Resource class not supported exception. - * - * @author Kévin Dunglas - */ -class ResourceClassNotSupportedException extends \Exception implements ExceptionInterface -{ -} diff --git a/src/Exception/RuntimeException.php b/src/Exception/RuntimeException.php deleted file mode 100644 index 7eba6481b10..00000000000 --- a/src/Exception/RuntimeException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Exception; - -/** - * Runtime exception. - * - * @author Kévin Dunglas - */ -class RuntimeException extends \RuntimeException implements ExceptionInterface -{ -} diff --git a/src/GraphQl/.gitattributes b/src/GraphQl/.gitattributes index ae3c2e1685a..801f2080d71 100644 --- a/src/GraphQl/.gitattributes +++ b/src/GraphQl/.gitattributes @@ -1,2 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore /.gitignore export-ignore /Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/GraphQl/.github/workflows/close_pr.yml b/src/GraphQl/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/GraphQl/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/GraphQl/Action/EntrypointAction.php b/src/GraphQl/Action/EntrypointAction.php index 672426ba30c..1c4fa44b68e 100644 --- a/src/GraphQl/Action/EntrypointAction.php +++ b/src/GraphQl/Action/EntrypointAction.php @@ -41,12 +41,10 @@ public function __construct( private readonly SchemaBuilderInterface $schemaBuilder, private readonly ExecutorInterface $executor, private readonly ?GraphiQlAction $graphiQlAction, - private readonly ?GraphQlPlaygroundAction $graphQlPlaygroundAction, private readonly NormalizerInterface $normalizer, private readonly ErrorHandlerInterface $errorHandler, bool $debug = false, private readonly bool $graphiqlEnabled = false, - private readonly bool $graphQlPlaygroundEnabled = false, private readonly ?string $defaultIde = null, ?Negotiator $negotiator = null, ) { @@ -64,10 +62,6 @@ public function __invoke(Request $request): Response if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled && $this->graphiQlAction) { return ($this->graphiQlAction)($request); } - - if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled && $this->graphQlPlaygroundAction) { - return ($this->graphQlPlaygroundAction)($request); - } } [$query, $operationName, $variables] = $this->parseRequest($request); diff --git a/src/GraphQl/Action/GraphQlPlaygroundAction.php b/src/GraphQl/Action/GraphQlPlaygroundAction.php deleted file mode 100644 index cc03e4a4a06..00000000000 --- a/src/GraphQl/Action/GraphQlPlaygroundAction.php +++ /dev/null @@ -1,45 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Action; - -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Component\Routing\RouterInterface; -use Twig\Environment as TwigEnvironment; - -/** - * GraphQL Playground entrypoint. - * - * @author Alan Poulain - */ -final class GraphQlPlaygroundAction -{ - public function __construct(private readonly TwigEnvironment $twig, private readonly RouterInterface $router, private readonly bool $graphQlPlaygroundEnabled = false, private readonly string $title = '', private $assetPackage = null) - { - } - - public function __invoke(Request $request): Response - { - if ($this->graphQlPlaygroundEnabled) { - return new Response($this->twig->render('@ApiPlatform/GraphQlPlayground/index.html.twig', [ - 'title' => $this->title, - 'graphql_playground_data' => ['entrypoint' => $this->router->generate('api_graphql_entrypoint')], - 'assetPackage' => $this->assetPackage, - ]), 200, ['content-type' => 'text/html']); - } - - throw new BadRequestHttpException('GraphQL Playground is not enabled.'); - } -} diff --git a/src/GraphQl/Action/GraphiQlAction.php b/src/GraphQl/Action/GraphiQlAction.php index dc6c0119483..ca67f91e030 100644 --- a/src/GraphQl/Action/GraphiQlAction.php +++ b/src/GraphQl/Action/GraphiQlAction.php @@ -26,6 +26,9 @@ */ final class GraphiQlAction { + /** + * @param string|null $assetPackage + */ public function __construct(private readonly TwigEnvironment $twig, private readonly RouterInterface $router, private readonly bool $graphiqlEnabled = false, private readonly string $title = '', private $assetPackage = null) { } diff --git a/src/GraphQl/Exception/InvalidTypeException.php b/src/GraphQl/Exception/InvalidTypeException.php new file mode 100644 index 00000000000..31241f627f4 --- /dev/null +++ b/src/GraphQl/Exception/InvalidTypeException.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\GraphQl\Exception; + +final class InvalidTypeException extends \RuntimeException +{ +} diff --git a/src/GraphQl/Executor.php b/src/GraphQl/Executor.php index 5c2ecddf7f1..36359a3534b 100644 --- a/src/GraphQl/Executor.php +++ b/src/GraphQl/Executor.php @@ -18,6 +18,8 @@ use GraphQL\Type\Schema; use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\DisableIntrospection; +use GraphQL\Validator\Rules\QueryComplexity; +use GraphQL\Validator\Rules\QueryDepth; /** * Wrapper for the GraphQL facade. @@ -26,13 +28,19 @@ */ final class Executor implements ExecutorInterface { - public function __construct(private readonly bool $graphQlIntrospectionEnabled = true) + public function __construct(private readonly bool $graphQlIntrospectionEnabled = true, private readonly int $maxQueryComplexity = 500, private readonly int $maxQueryDepth = 20) { DocumentValidator::addRule( new DisableIntrospection( $this->graphQlIntrospectionEnabled ? DisableIntrospection::DISABLED : DisableIntrospection::ENABLED ) ); + + $queryComplexity = new QueryComplexity($this->maxQueryComplexity); + DocumentValidator::addRule($queryComplexity); + + $queryDepth = new QueryDepth($this->maxQueryDepth); + DocumentValidator::addRule($queryDepth); } /** diff --git a/src/GraphQl/ExecutorInterface.php b/src/GraphQl/ExecutorInterface.php index fafc326ee6f..180dbdaa390 100644 --- a/src/GraphQl/ExecutorInterface.php +++ b/src/GraphQl/ExecutorInterface.php @@ -25,6 +25,8 @@ interface ExecutorInterface { /** * @see http://webonyx.github.io/graphql-php/executing-queries/#using-facade-method + * + * @param mixed $source */ public function executeQuery(Schema $schema, $source, mixed $rootValue = null, mixed $context = null, ?array $variableValues = null, ?string $operationName = null, ?callable $fieldResolver = null, ?array $validationRules = null): ExecutionResult; } diff --git a/src/GraphQl/Metadata/RuntimeOperationMetadataFactory.php b/src/GraphQl/Metadata/RuntimeOperationMetadataFactory.php new file mode 100644 index 00000000000..6b280da24cf --- /dev/null +++ b/src/GraphQl/Metadata/RuntimeOperationMetadataFactory.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\GraphQl\Metadata; + +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface; +use Symfony\Component\Routing\RouterInterface; + +/** + * This factory runs in the ResolverFactory and is used to find out a Relay node's operation. + */ +final class RuntimeOperationMetadataFactory implements OperationMetadataFactoryInterface +{ + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly RouterInterface $router) + { + } + + public function create(string $uriTemplate, array $context = []): Operation + { + try { + $parameters = $this->router->match($uriTemplate); + } catch (RoutingExceptionInterface $e) { + throw new InvalidArgumentException(\sprintf('No route matches "%s".', $uriTemplate), $e->getCode(), $e); + } + + if (!isset($parameters['_api_resource_class'])) { + throw new InvalidArgumentException(\sprintf('The route "%s" is not an API route, it has no resource class in the defaults.', $uriTemplate)); + } + + foreach ($this->resourceMetadataCollectionFactory->create($parameters['_api_resource_class']) as $resource) { + foreach ($resource->getGraphQlOperations() ?? [] as $operation) { + if ($operation instanceof Query && !$operation->getResolver()) { + return $operation; + } + } + } + + throw new InvalidArgumentException(\sprintf('No operation found for id "%s".', $uriTemplate)); + } +} diff --git a/src/GraphQl/README.md b/src/GraphQl/README.md index 10b0b8c8ae1..f46cd955f55 100644 --- a/src/GraphQl/README.md +++ b/src/GraphQl/README.md @@ -1,3 +1,12 @@ # API Platform - GraphQL -Build GraphQL API endpoints +The [GraphQL](https://graphql.org/) component of the [API Platform](https://api-platform.com) framework. + +[Documentation](https://api-platform.com/docs/core/graphql/) + +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php b/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php deleted file mode 100644 index 5f7e31f2af0..00000000000 --- a/src/GraphQl/Resolver/Factory/CollectionResolverFactory.php +++ /dev/null @@ -1,92 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\CloneTrait; -use ApiPlatform\State\Pagination\ArrayPaginator; -use GraphQL\Type\Definition\ResolveInfo; -use Psr\Container\ContainerInterface; - -/** - * Creates a function retrieving a collection to resolve a GraphQL query or a field returned by a mutation. - * - * @author Alan Poulain - * @author Kévin Dunglas - * @author Vincent Chalamon - */ -final class CollectionResolverFactory implements ResolverFactoryInterface -{ - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly ContainerInterface $queryResolverLocator) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { - // If authorization has failed for a relation field (e.g. via ApiProperty security), the field is not present in the source: null can be returned directly to ensure the collection isn't in the response. - if (null === $resourceClass || null === $rootClass || (null !== $source && !\array_key_exists($info->fieldName, $source))) { - return null; - } - - if (is_a($resourceClass, \BackedEnum::class, true) && $source && \array_key_exists($info->fieldName, $source)) { - return $source[$info->fieldName]; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - if ($operation instanceof Query && $operation->getNested() && !$operation->getResolver() && !$operation->getProvider() && $source && \array_key_exists($info->fieldName, $source)) { - return ($this->serializeStage)(new ArrayPaginator($source[$info->fieldName], 0, \count($source[$info->fieldName])), $resourceClass, $operation, $resolverContext); - } - - $collection = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (!is_iterable($collection)) { - throw new \LogicException('Collection from read stage should be iterable.'); - } - - $queryResolverId = $operation->getResolver(); - if (null !== $queryResolverId) { - /** @var QueryCollectionResolverInterface $queryResolver */ - $queryResolver = $this->queryResolverLocator->get($queryResolverId); - $collection = $queryResolver($collection, $resolverContext); - } - - // Only perform security stage on the top-level query - if (null === $source) { - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $collection, - ], - ]); - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $collection, - 'previous_object' => $this->clone($collection), - ], - ]); - } - - return ($this->serializeStage)($collection, $resourceClass, $operation, $resolverContext); - }; - } -} diff --git a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php deleted file mode 100644 index dddc15897dc..00000000000 --- a/src/GraphQl/Resolver/Factory/ItemMutationResolverFactory.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\MutationResolverInterface; -use ApiPlatform\GraphQl\Resolver\Stage\DeserializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ValidateStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface; -use ApiPlatform\Metadata\DeleteOperationInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Metadata\Util\CloneTrait; -use GraphQL\Type\Definition\ResolveInfo; -use Psr\Container\ContainerInterface; - -/** - * Creates a function resolving a GraphQL mutation of an item. - * - * @author Alan Poulain - * @author Vincent Chalamon - */ -final class ItemMutationResolverFactory implements ResolverFactoryInterface -{ - use ClassInfoTrait; - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly DeserializeStageInterface $deserializeStage, private readonly WriteStageInterface $writeStage, private readonly ValidateStageInterface $validateStage, private readonly ContainerInterface $mutationResolverLocator, private readonly SecurityPostValidationStageInterface $securityPostValidationStage) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { - if (null === $resourceClass || null === $operation) { - return null; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $item = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (null !== $item && !\is_object($item)) { - throw new \LogicException('Item from read stage should be a nullable object.'); - } - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - ], - ]); - $previousItem = $this->clone($item); - - if ('delete' === $operation->getName() || $operation instanceof DeleteOperationInterface) { - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $previousItem, - ], - ]); - $item = ($this->writeStage)($item, $resourceClass, $operation, $resolverContext); - - return ($this->serializeStage)($item, $resourceClass, $operation, $resolverContext); - } - - $item = ($this->deserializeStage)($item, $resourceClass, $operation, $resolverContext); - - $mutationResolverId = $operation->getResolver(); - if (null !== $mutationResolverId) { - /** @var MutationResolverInterface $mutationResolver */ - $mutationResolver = $this->mutationResolverLocator->get($mutationResolverId); - $item = $mutationResolver($item, $resolverContext); - if (null !== $item && $resourceClass !== $itemClass = $this->getObjectClass($item)) { - throw new \LogicException(\sprintf('Custom mutation resolver "%s" has to return an item of class %s but returned an item of class %s.', $mutationResolverId, $operation->getShortName(), (new \ReflectionClass($itemClass))->getShortName())); - } - } - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $previousItem, - ], - ]); - - if (null !== $item) { - ($this->validateStage)($item, $resourceClass, $operation, $resolverContext); - - ($this->securityPostValidationStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $previousItem, - ], - ]); - - $persistResult = ($this->writeStage)($item, $resourceClass, $operation, $resolverContext); - } - - return ($this->serializeStage)($persistResult ?? $item, $resourceClass, $operation, $resolverContext); - }; - } -} diff --git a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemResolverFactory.php deleted file mode 100644 index 08a7c280d59..00000000000 --- a/src/GraphQl/Resolver/Factory/ItemResolverFactory.php +++ /dev/null @@ -1,116 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Metadata\Util\CloneTrait; -use GraphQL\Type\Definition\ResolveInfo; -use Psr\Container\ContainerInterface; - -/** - * Creates a function retrieving an item to resolve a GraphQL query. - * - * @author Alan Poulain - * @author Kévin Dunglas - * @author Vincent Chalamon - */ -final class ItemResolverFactory implements ResolverFactoryInterface -{ - use ClassInfoTrait; - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SecurityPostDenormalizeStageInterface $securityPostDenormalizeStage, private readonly SerializeStageInterface $serializeStage, private readonly ContainerInterface $queryResolverLocator) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation) { - // Data already fetched and normalized (field or nested resource) - if ($source && \array_key_exists($info->fieldName, $source)) { - return $source[$info->fieldName]; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - if (!$operation) { - $operation = new Query(); - } - - $item = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (null !== $item && !\is_object($item)) { - throw new \LogicException('Item from read stage should be a nullable object.'); - } - - $resourceClass = $operation->getOutput()['class'] ?? $resourceClass; - // The item retrieved can be of another type when using an identifier (see Relay Nodes at query.feature:23) - $resourceClass = $this->getResourceClass($item, $resourceClass); - $queryResolverId = $operation->getResolver(); - if (null !== $queryResolverId) { - /** @var QueryItemResolverInterface $queryResolver */ - $queryResolver = $this->queryResolverLocator->get($queryResolverId); - $item = $queryResolver($item, $resolverContext); - $resourceClass = $this->getResourceClass($item, $resourceClass, \sprintf('Custom query resolver "%s"', $queryResolverId).' has to return an item of class %s but returned an item of class %s.'); - } - - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - ], - ]); - ($this->securityPostDenormalizeStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - 'previous_object' => $this->clone($item), - ], - ]); - - return ($this->serializeStage)($item, $resourceClass, $operation, $resolverContext); - }; - } - - /** - * @throws \UnexpectedValueException - */ - private function getResourceClass(?object $item, ?string $resourceClass, string $errorMessage = 'Resolver only handles items of class %s but retrieved item is of class %s.'): string - { - if (null === $item) { - if (null === $resourceClass) { - throw new \UnexpectedValueException('Resource class cannot be determined.'); - } - - return $resourceClass; - } - - $itemClass = $this->getObjectClass($item); - - if (null === $resourceClass) { - return $itemClass; - } - - if ($resourceClass !== $itemClass && !$item instanceof $resourceClass) { - throw new \UnexpectedValueException(\sprintf($errorMessage, (new \ReflectionClass($resourceClass))->getShortName(), (new \ReflectionClass($itemClass))->getShortName())); - } - - return $resourceClass; - } -} diff --git a/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php b/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php deleted file mode 100644 index b182ba528ff..00000000000 --- a/src/GraphQl/Resolver/Factory/ItemSubscriptionResolverFactory.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface; -use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Metadata\Util\CloneTrait; -use GraphQL\Type\Definition\ResolveInfo; - -/** - * Creates a function resolving a GraphQL subscription of an item. - * - * @author Alan Poulain - */ -final class ItemSubscriptionResolverFactory implements ResolverFactoryInterface -{ - use ClassInfoTrait; - use CloneTrait; - - public function __construct(private readonly ReadStageInterface $readStage, private readonly SecurityStageInterface $securityStage, private readonly SerializeStageInterface $serializeStage, private readonly SubscriptionManagerInterface $subscriptionManager, private readonly ?MercureSubscriptionIriGeneratorInterface $mercureSubscriptionIriGenerator) - { - } - - public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable - { - return function (?array $source, array $args, $context, ResolveInfo $info) use ($resourceClass, $rootClass, $operation): ?array { - if (null === $resourceClass || null === $operation) { - return null; - } - - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $item = ($this->readStage)($resourceClass, $rootClass, $operation, $resolverContext); - if (null !== $item && !\is_object($item)) { - throw new \LogicException('Item from read stage should be a nullable object.'); - } - ($this->securityStage)($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $item, - ], - ]); - - $result = ($this->serializeStage)($item, $resourceClass, $operation, $resolverContext); - - $subscriptionId = $this->subscriptionManager->retrieveSubscriptionId($resolverContext, $result); - - if ($subscriptionId && ($mercure = $operation->getMercure())) { - if (!$this->mercureSubscriptionIriGenerator) { - throw new \LogicException('Cannot use Mercure for subscriptions when MercureBundle is not installed. Try running "composer require mercure".'); - } - - $hub = \is_array($mercure) ? ($mercure['hub'] ?? null) : null; - $result['mercureUrl'] = $this->mercureSubscriptionIriGenerator->generateMercureUrl($subscriptionId, $hub); - } - - return $result; - }; - } -} diff --git a/src/GraphQl/Resolver/Factory/ResolverFactory.php b/src/GraphQl/Resolver/Factory/ResolverFactory.php index c7ba03283ee..8d129314ca3 100644 --- a/src/GraphQl/Resolver/Factory/ResolverFactory.php +++ b/src/GraphQl/Resolver/Factory/ResolverFactory.php @@ -15,21 +15,30 @@ use ApiPlatform\GraphQl\State\Provider\NoopProvider; use ApiPlatform\Metadata\DeleteOperationInterface; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\State\Pagination\ArrayPaginator; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; use GraphQL\Type\Definition\ResolveInfo; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\TypeInfo\Type\CollectionType; class ResolverFactory implements ResolverFactoryInterface { public function __construct( private readonly ProviderInterface $provider, private readonly ProcessorInterface $processor, + private readonly ?OperationMetadataFactoryInterface $operationMetadataFactory = null, ) { + if (!$operationMetadataFactory) { + throw new InvalidArgumentException(\sprintf('Not injecting the "%s" exposes Relay nodes to a security risk.', OperationMetadataFactoryInterface::class)); + } } public function __invoke(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?PropertyMetadataFactoryInterface $propertyMetadataFactory = null): callable @@ -51,10 +60,21 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul } $propertyMetadata = $rootClass ? $propertyMetadataFactory?->create($rootClass, $info->fieldName) : null; - $type = $propertyMetadata?->getBuiltinTypes()[0] ?? null; - // Data already fetched and normalized (field or nested resource) - if ($body || null === $resourceClass || ($type && !$type->isCollection())) { - return $body; + + if (method_exists(PropertyInfoExtractor::class, 'getType')) { + $type = $propertyMetadata?->getNativeType(); + + // Data already fetched and normalized (field or nested resource) + if ($body || null === $resourceClass || ($type && !$type->isSatisfiedBy(fn ($t) => $t instanceof CollectionType))) { + return $body; + } + } else { + $type = $propertyMetadata?->getBuiltinTypes()[0] ?? null; + + // Data already fetched and normalized (field or nested resource) + if ($body || null === $resourceClass || ($type && !$type->isCollection())) { + return $body; + } } } @@ -67,10 +87,16 @@ public function __invoke(?string $resourceClass = null, ?string $rootClass = nul }; } - private function resolve(?array $source, array $args, ResolveInfo $info, ?string $rootClass = null, ?Operation $operation = null, mixed $body = null) + private function resolve(?array $source, array $args, ResolveInfo $info, ?string $rootClass = null, ?Operation $operation = null, mixed $body = null): mixed { // Handles relay nodes - $operation ??= new Query(); + if (!$operation) { + if (!isset($args['id'])) { + throw new NotFoundHttpException('No node found.'); + } + + $operation = $this->operationMetadataFactory->create($args['id']); + } $graphQlContext = []; $context = ['source' => $source, 'args' => $args, 'info' => $info, 'root_class' => $rootClass, 'graphql_context' => &$graphQlContext]; diff --git a/src/GraphQl/Resolver/ResourceFieldResolver.php b/src/GraphQl/Resolver/ResourceFieldResolver.php index 54b08218928..59e7a1596ab 100644 --- a/src/GraphQl/Resolver/ResourceFieldResolver.php +++ b/src/GraphQl/Resolver/ResourceFieldResolver.php @@ -32,6 +32,9 @@ public function __construct(private readonly IriConverterInterface $iriConverter { } + /** + * @param array $context + */ public function __invoke(?array $source, array $args, $context, ResolveInfo $info): mixed { $property = null; diff --git a/src/GraphQl/Resolver/Stage/DeserializeStage.php b/src/GraphQl/Resolver/Stage/DeserializeStage.php deleted file mode 100644 index 0e7f7daec1e..00000000000 --- a/src/GraphQl/Resolver/Stage/DeserializeStage.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * Deserialize stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class DeserializeStage implements DeserializeStageInterface -{ - public function __construct(private readonly DenormalizerInterface $denormalizer, private readonly SerializerContextBuilderInterface $serializerContextBuilder) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(?object $objectToPopulate, string $resourceClass, Operation $operation, array $context): ?object - { - if (!($operation->canDeserialize() ?? true)) { - return $objectToPopulate; - } - - $denormalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, false); - if (null !== $objectToPopulate) { - $denormalizationContext[AbstractNormalizer::OBJECT_TO_POPULATE] = $objectToPopulate; - } - - $item = $this->denormalizer->denormalize($context['args']['input'], $resourceClass, ItemNormalizer::FORMAT, $denormalizationContext); - - if (!\is_object($item)) { - throw new \UnexpectedValueException('Expected item to be an object.'); - } - - return $item; - } -} diff --git a/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php b/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php deleted file mode 100644 index 586104961c4..00000000000 --- a/src/GraphQl/Resolver/Stage/DeserializeStageInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Deserialize stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -interface DeserializeStageInterface -{ - public function __invoke(?object $objectToPopulate, string $resourceClass, Operation $operation, array $context): ?object; -} diff --git a/src/GraphQl/Resolver/Stage/ReadStage.php b/src/GraphQl/Resolver/Stage/ReadStage.php deleted file mode 100644 index cf913533a75..00000000000 --- a/src/GraphQl/Resolver/Stage/ReadStage.php +++ /dev/null @@ -1,190 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\Exception\ItemNotFoundException; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\State\ProviderInterface; -use GraphQL\Type\Definition\ResolveInfo; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -/** - * Read stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class ReadStage implements ReadStageInterface -{ - use IdentifierTrait; - - public function __construct(private readonly IriConverterInterface $iriConverter, private readonly ProviderInterface $provider, private readonly SerializerContextBuilderInterface $serializerContextBuilder, private readonly string $nestingSeparator) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(?string $resourceClass, ?string $rootClass, Operation $operation, array $context): object|array|null - { - if (!($operation->canRead() ?? true)) { - return $context['is_collection'] ? [] : null; - } - - $args = $context['args']; - $normalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, true); - - if (!$context['is_collection']) { - $identifier = $this->getIdentifierFromContext($context); - $item = $this->getItem($identifier, $normalizationContext); - - if ($identifier && ($context['is_mutation'] || $context['is_subscription'])) { - if (null === $item) { - throw new NotFoundHttpException(\sprintf('Item "%s" not found.', $args['input']['id'])); - } - - if ($resourceClass !== $this->getObjectClass($item)) { - throw new \UnexpectedValueException(\sprintf('Item "%s" did not match expected type "%s".', $args['input']['id'], $operation->getShortName())); - } - } - - return $item; - } - - if (null === $rootClass) { - return []; - } - - $uriVariables = []; - $normalizationContext['filters'] = $this->getNormalizedFilters($args); - $normalizationContext['operation'] = $operation; - - $source = $context['source']; - /** @var ResolveInfo $info */ - $info = $context['info']; - if (isset($source[$info->fieldName], $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY], $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY])) { - $uriVariables = $source[ItemNormalizer::ITEM_IDENTIFIERS_KEY]; - $normalizationContext['linkClass'] = $source[ItemNormalizer::ITEM_RESOURCE_CLASS_KEY]; - $normalizationContext['linkProperty'] = $info->fieldName; - } - - return $this->provider->provide($operation, $uriVariables, $normalizationContext); - } - - private function getItem(?string $identifier, array $normalizationContext): ?object - { - if (null === $identifier) { - return null; - } - - try { - $item = $this->iriConverter->getResourceFromIri($identifier, $normalizationContext); - } catch (ItemNotFoundException) { - return null; - } - - return $item; - } - - private function getNormalizedFilters(array $args): array - { - $filters = $args; - - foreach ($filters as $name => $value) { - if (\is_array($value)) { - if (strpos($name, '_list')) { - $name = substr($name, 0, \strlen($name) - \strlen('_list')); - } - - // If the value contains arrays, we need to merge them for the filters to understand this syntax, proper to GraphQL to preserve the order of the arguments. - if ($this->isSequentialArrayOfArrays($value)) { - $value = array_merge(...$value); - } - $filters[$name] = $this->getNormalizedFilters($value); - } - - if (\is_string($name) && strpos($name, $this->nestingSeparator)) { - // Gives a chance to relations/nested fields. - $index = array_search($name, array_keys($filters), true); - $filters = - \array_slice($filters, 0, $index + 1) + - [str_replace($this->nestingSeparator, '.', $name) => $value] + - \array_slice($filters, $index + 1); - } - } - - return $filters; - } - - public function isSequentialArrayOfArrays(array $array): bool - { - if (!$this->isSequentialArray($array)) { - return false; - } - - return $this->arrayContainsOnly($array, 'array'); - } - - public function isSequentialArray(array $array): bool - { - if ([] === $array) { - return false; - } - - return array_is_list($array); - } - - public function arrayContainsOnly(array $array, string $type): bool - { - return $array === array_filter($array, static fn ($item): bool => $type === \gettype($item)); - } - - /** - * Get class name of the given object. - */ - private function getObjectClass(object $object): string - { - return $this->getRealClassName($object::class); - } - - /** - * Get the real class name of a class name that could be a proxy. - */ - private function getRealClassName(string $className): string - { - // __CG__: Doctrine Common Marker for Proxy (ODM < 2.0 and ORM < 3.0) - // __PM__: Ocramius Proxy Manager (ODM >= 2.0) - $positionCg = strrpos($className, '\\__CG__\\'); - $positionPm = strrpos($className, '\\__PM__\\'); - - if (false === $positionCg && false === $positionPm) { - return $className; - } - - if (false !== $positionCg) { - return substr($className, $positionCg + 8); - } - - $className = ltrim($className, '\\'); - - return substr( - $className, - 8 + $positionPm, - strrpos($className, '\\') - ($positionPm + 8) - ); - } -} diff --git a/src/GraphQl/Resolver/Stage/ReadStageInterface.php b/src/GraphQl/Resolver/Stage/ReadStageInterface.php deleted file mode 100644 index fa0e941ea20..00000000000 --- a/src/GraphQl/Resolver/Stage/ReadStageInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Read stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -interface ReadStageInterface -{ - public function __invoke(?string $resourceClass, ?string $rootClass, Operation $operation, array $context): object|array|null; -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php deleted file mode 100644 index f87d48ed545..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStage.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface as LegacyResourceAccessCheckerInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * Security post denormalize stage of GraphQL resolvers. - * - * @author Vincent Chalamon - */ -final class SecurityPostDenormalizeStage implements SecurityPostDenormalizeStageInterface -{ - /** - * @var LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface - */ - private $resourceAccessChecker; - - /** - * @param LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface|null $resourceAccessChecker - */ - public function __construct($resourceAccessChecker) - { - $this->resourceAccessChecker = $resourceAccessChecker; - } - - /** - * {@inheritdoc} - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void - { - $isGranted = $operation->getSecurityPostDenormalize(); - - if (null !== $isGranted && null === $this->resourceAccessChecker) { - throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); - } - - if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { - return; - } - - throw new AccessDeniedHttpException($operation->getSecurityPostDenormalizeMessage() ?? 'Access Denied.'); - } -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php deleted file mode 100644 index a62ba52731d..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostDenormalizeStageInterface.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Security post deserialization stage of GraphQL resolvers. - * - * @author Vincent Chalamon - */ -interface SecurityPostDenormalizeStageInterface -{ - /** - * @throws Error - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostValidationStage.php b/src/GraphQl/Resolver/Stage/SecurityPostValidationStage.php deleted file mode 100644 index 4ab18fa8716..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostValidationStage.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface as LegacyResourceAccessCheckerInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * Security post validation stage of GraphQL resolvers. - * - * @deprecated use providers instead of stages - * - * @author Vincent Chalamon - * @author Grégoire Pineau - */ -final class SecurityPostValidationStage implements SecurityPostValidationStageInterface -{ - /** - * @var LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface - */ - private $resourceAccessChecker; - - /** - * @param LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface|null $resourceAccessChecker - */ - public function __construct($resourceAccessChecker) - { - $this->resourceAccessChecker = $resourceAccessChecker; - } - - /** - * {@inheritdoc} - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void - { - $isGranted = $operation->getSecurityPostValidation(); - - if (null !== $isGranted && null === $this->resourceAccessChecker) { - throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); - } - - if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { - return; - } - - throw new AccessDeniedHttpException($operation->getSecurityPostValidationMessage() ?? 'Access Denied.'); - } -} diff --git a/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php deleted file mode 100644 index bbbbf7d4d05..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityPostValidationStageInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Security post validation stage of GraphQL resolvers. - * - * @author Vincent Chalamon - * @author Grégoire Pineau - */ -interface SecurityPostValidationStageInterface -{ - /** - * @throws Error - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/SecurityStage.php b/src/GraphQl/Resolver/Stage/SecurityStage.php deleted file mode 100644 index b577fc1c8a0..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityStage.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface as LegacyResourceAccessCheckerInterface; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * Security stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class SecurityStage implements SecurityStageInterface -{ - /** - * @var LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface - */ - private $resourceAccessChecker; - - /** - * @param LegacyResourceAccessCheckerInterface|ResourceAccessCheckerInterface|null $resourceAccessChecker - */ - public function __construct($resourceAccessChecker) - { - $this->resourceAccessChecker = $resourceAccessChecker; - } - - /** - * {@inheritdoc} - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void - { - $isGranted = $operation->getSecurity(); - - if (null !== $isGranted && null === $this->resourceAccessChecker) { - throw new \LogicException('Cannot check security expression when SecurityBundle is not installed. Try running "composer require symfony/security-bundle".'); - } - - if (null === $isGranted || $this->resourceAccessChecker->isGranted($resourceClass, (string) $isGranted, $context['extra_variables'])) { - return; - } - - throw new AccessDeniedHttpException($operation->getSecurityMessage() ?? 'Access Denied.'); - } -} diff --git a/src/GraphQl/Resolver/Stage/SecurityStageInterface.php b/src/GraphQl/Resolver/Stage/SecurityStageInterface.php deleted file mode 100644 index a1f47fad9bf..00000000000 --- a/src/GraphQl/Resolver/Stage/SecurityStageInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Security stage of GraphQL resolvers. - * - * @author Alan Poulain - * @author Vincent Chalamon - */ -interface SecurityStageInterface -{ - /** - * @throws Error - */ - public function __invoke(string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/SerializeStage.php b/src/GraphQl/Resolver/Stage/SerializeStage.php deleted file mode 100644 index 60f85d96deb..00000000000 --- a/src/GraphQl/Resolver/Stage/SerializeStage.php +++ /dev/null @@ -1,244 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Subscription; -use ApiPlatform\State\Pagination\HasNextPagePaginatorInterface; -use ApiPlatform\State\Pagination\Pagination; -use ApiPlatform\State\Pagination\PaginatorInterface; -use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * Serialize stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class SerializeStage implements SerializeStageInterface -{ - use IdentifierTrait; - - public function __construct(private readonly NormalizerInterface $normalizer, private readonly SerializerContextBuilderInterface $serializerContextBuilder, private readonly Pagination $pagination) - { - } - - public function __invoke(object|array|null $itemOrCollection, string $resourceClass, Operation $operation, array $context): ?array - { - $isCollection = $operation instanceof CollectionOperationInterface; - $isMutation = $operation instanceof Mutation; - $isSubscription = $operation instanceof Subscription; - $shortName = $operation->getShortName(); - $operationName = $operation->getName(); - - if (!($operation->canSerialize() ?? true)) { - if ($isCollection) { - if ($this->pagination->isGraphQlEnabled($operation, $context)) { - return 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ? - $this->getDefaultCursorBasedPaginatedData() : - $this->getDefaultPageBasedPaginatedData(); - } - - return []; - } - - if ($isMutation) { - return $this->getDefaultMutationData($context); - } - - if ($isSubscription) { - return $this->getDefaultSubscriptionData($context); - } - - return null; - } - - $normalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, true); - - $data = null; - if (!$isCollection) { - if ($isMutation && 'delete' === $operationName) { - $data = ['id' => $this->getIdentifierFromContext($context)]; - } else { - $data = $this->normalizer->normalize($itemOrCollection, ItemNormalizer::FORMAT, $normalizationContext); - } - } - - if ($isCollection && is_iterable($itemOrCollection)) { - if (!$this->pagination->isGraphQlEnabled($operation, $context)) { - $data = []; - foreach ($itemOrCollection as $index => $object) { - $data[$index] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext); - } - } else { - $data = 'cursor' === $this->pagination->getGraphQlPaginationType($operation) ? - $this->serializeCursorBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context) : - $this->serializePageBasedPaginatedCollection($itemOrCollection, $normalizationContext, $context); - } - } - - if (null !== $data && !\is_array($data)) { - throw new \UnexpectedValueException('Expected serialized data to be a nullable array.'); - } - - if ($isMutation || $isSubscription) { - $wrapFieldName = lcfirst($shortName); - - return [$wrapFieldName => $data] + ($isMutation ? $this->getDefaultMutationData($context) : $this->getDefaultSubscriptionData($context)); - } - - return $data; - } - - /** - * @throws \LogicException - * @throws \UnexpectedValueException - */ - private function serializeCursorBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array - { - $args = $context['args']; - - if (!($collection instanceof PartialPaginatorInterface)) { - throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s or %s.', PaginatorInterface::class, PartialPaginatorInterface::class)); - } - - $selection = $context['info']->getFieldSelection(1); - - $offset = 0; - $totalItems = 1; // For partial pagination, always consider there is at least one item. - $data = ['edges' => []]; - if (isset($selection['pageInfo']) || isset($selection['totalCount']) || isset($selection['edges']['cursor'])) { - $nbPageItems = $collection->count(); - if (isset($args['after'])) { - $after = base64_decode($args['after'], true); - if (false === $after || '' === $args['after']) { - throw new \UnexpectedValueException('' === $args['after'] ? 'Empty cursor is invalid' : \sprintf('Cursor %s is invalid', $args['after'])); - } - $offset = 1 + (int) $after; - } - - if ($collection instanceof PaginatorInterface && (isset($selection['pageInfo']) || isset($selection['totalCount']))) { - $totalItems = $collection->getTotalItems(); - if (isset($args['before'])) { - $before = base64_decode($args['before'], true); - if (false === $before || '' === $args['before']) { - throw new \UnexpectedValueException('' === $args['before'] ? 'Empty cursor is invalid' : \sprintf('Cursor %s is invalid', $args['before'])); - } - $offset = (int) $before - $nbPageItems; - } - if (isset($args['last']) && !isset($args['before'])) { - $offset = $totalItems - $args['last']; - } - } - - $offset = max(0, $offset); - - $data = $this->getDefaultCursorBasedPaginatedData(); - if ((isset($selection['pageInfo']) || isset($selection['totalCount'])) && $totalItems > 0) { - isset($selection['pageInfo']['startCursor']) && $data['pageInfo']['startCursor'] = base64_encode((string) $offset); - $end = $offset + $nbPageItems - 1; - isset($selection['pageInfo']['endCursor']) && $data['pageInfo']['endCursor'] = base64_encode((string) max($end, 0)); - isset($selection['pageInfo']['hasPreviousPage']) && $data['pageInfo']['hasPreviousPage'] = $offset > 0; - if ($collection instanceof PaginatorInterface) { - isset($selection['totalCount']) && $data['totalCount'] = $totalItems; - - $itemsPerPage = $collection->getItemsPerPage(); - isset($selection['pageInfo']['hasNextPage']) && $data['pageInfo']['hasNextPage'] = (float) ($itemsPerPage > 0 ? $offset % $itemsPerPage : $offset) + $itemsPerPage * $collection->getCurrentPage() < $totalItems; - } - } - } - - $index = 0; - foreach ($collection as $object) { - $edge = [ - 'node' => $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext), - ]; - if (isset($selection['edges']['cursor'])) { - $edge['cursor'] = base64_encode((string) ($index + $offset)); - } - $data['edges'][$index] = $edge; - ++$index; - } - - return $data; - } - - /** - * @throws \LogicException - */ - private function serializePageBasedPaginatedCollection(iterable $collection, array $normalizationContext, array $context): array - { - $data = ['collection' => []]; - - $selection = $context['info']->getFieldSelection(1); - if (isset($selection['paginationInfo'])) { - $data['paginationInfo'] = []; - if (isset($selection['paginationInfo']['itemsPerPage'])) { - if (!($collection instanceof PartialPaginatorInterface)) { - throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return itemsPerPage field.', PartialPaginatorInterface::class)); - } - $data['paginationInfo']['itemsPerPage'] = $collection->getItemsPerPage(); - } - if (isset($selection['paginationInfo']['totalCount'])) { - if (!($collection instanceof PaginatorInterface)) { - throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return totalCount field.', PaginatorInterface::class)); - } - $data['paginationInfo']['totalCount'] = $collection->getTotalItems(); - } - if (isset($selection['paginationInfo']['lastPage'])) { - if (!($collection instanceof PaginatorInterface)) { - throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return lastPage field.', PaginatorInterface::class)); - } - $data['paginationInfo']['lastPage'] = $collection->getLastPage(); - } - if (isset($selection['paginationInfo']['hasNextPage'])) { - if (!($collection instanceof HasNextPagePaginatorInterface)) { - throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return hasNextPage field.', HasNextPagePaginatorInterface::class)); - } - $data['paginationInfo']['hasNextPage'] = $collection->hasNextPage(); - } - } - - foreach ($collection as $object) { - $data['collection'][] = $this->normalizer->normalize($object, ItemNormalizer::FORMAT, $normalizationContext); - } - - return $data; - } - - private function getDefaultCursorBasedPaginatedData(): array - { - return ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]]; - } - - private function getDefaultPageBasedPaginatedData(): array - { - return ['collection' => [], 'paginationInfo' => ['itemsPerPage' => 0., 'totalCount' => 0., 'lastPage' => 0., 'hasNextPage' => false]]; - } - - private function getDefaultMutationData(array $context): array - { - return ['clientMutationId' => $context['args']['input']['clientMutationId'] ?? null]; - } - - private function getDefaultSubscriptionData(array $context): array - { - return ['clientSubscriptionId' => $context['args']['input']['clientSubscriptionId'] ?? null]; - } -} diff --git a/src/GraphQl/Resolver/Stage/SerializeStageInterface.php b/src/GraphQl/Resolver/Stage/SerializeStageInterface.php deleted file mode 100644 index ac67e9b97f0..00000000000 --- a/src/GraphQl/Resolver/Stage/SerializeStageInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Serialize stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -interface SerializeStageInterface -{ - public function __invoke(object|array|null $itemOrCollection, string $resourceClass, Operation $operation, array $context): ?array; -} diff --git a/src/GraphQl/Resolver/Stage/ValidateStage.php b/src/GraphQl/Resolver/Stage/ValidateStage.php deleted file mode 100644 index c7c2103b4b3..00000000000 --- a/src/GraphQl/Resolver/Stage/ValidateStage.php +++ /dev/null @@ -1,41 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Validator\ValidatorInterface; - -/** - * Validate stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class ValidateStage implements ValidateStageInterface -{ - public function __construct(private readonly ValidatorInterface $validator) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(object $object, string $resourceClass, Operation $operation, array $context): void - { - if (!($operation->canValidate() ?? true)) { - return; - } - - $this->validator->validate($object, $operation->getValidationContext() ?? []); - } -} diff --git a/src/GraphQl/Resolver/Stage/ValidateStageInterface.php b/src/GraphQl/Resolver/Stage/ValidateStageInterface.php deleted file mode 100644 index 092a1d5aee4..00000000000 --- a/src/GraphQl/Resolver/Stage/ValidateStageInterface.php +++ /dev/null @@ -1,30 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; -use GraphQL\Error\Error; - -/** - * Validate stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -interface ValidateStageInterface -{ - /** - * @throws Error - */ - public function __invoke(object $object, string $resourceClass, Operation $operation, array $context): void; -} diff --git a/src/GraphQl/Resolver/Stage/WriteStage.php b/src/GraphQl/Resolver/Stage/WriteStage.php deleted file mode 100644 index 35bd780154b..00000000000 --- a/src/GraphQl/Resolver/Stage/WriteStage.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\State\ProcessorInterface; - -/** - * Write stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -final class WriteStage implements WriteStageInterface -{ - public function __construct(private readonly ProcessorInterface $processor, private readonly SerializerContextBuilderInterface $serializerContextBuilder) - { - } - - /** - * {@inheritdoc} - */ - public function __invoke(?object $data, string $resourceClass, Operation $operation, array $context): ?object - { - if (null === $data || !($operation->canWrite() ?? true)) { - return $data; - } - - $denormalizationContext = $this->serializerContextBuilder->create($resourceClass, $operation, $context, false); - - return $this->processor->process($data, $operation, [], ['operation' => $operation] + $denormalizationContext); - } -} diff --git a/src/GraphQl/Resolver/Stage/WriteStageInterface.php b/src/GraphQl/Resolver/Stage/WriteStageInterface.php deleted file mode 100644 index 9d090ce59c9..00000000000 --- a/src/GraphQl/Resolver/Stage/WriteStageInterface.php +++ /dev/null @@ -1,26 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Resolver\Stage; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Write stage of GraphQL resolvers. - * - * @author Alan Poulain - */ -interface WriteStageInterface -{ - public function __invoke(?object $data, string $resourceClass, Operation $operation, array $context): ?object; -} diff --git a/src/GraphQl/Serializer/Exception/ErrorNormalizer.php b/src/GraphQl/Serializer/Exception/ErrorNormalizer.php index ec9ecf772a8..47e8721df45 100644 --- a/src/GraphQl/Serializer/Exception/ErrorNormalizer.php +++ b/src/GraphQl/Serializer/Exception/ErrorNormalizer.php @@ -40,6 +40,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return $data instanceof Error; } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { return [ diff --git a/src/GraphQl/Serializer/Exception/HttpExceptionNormalizer.php b/src/GraphQl/Serializer/Exception/HttpExceptionNormalizer.php index 3f16e4ef46c..86b3242b159 100644 --- a/src/GraphQl/Serializer/Exception/HttpExceptionNormalizer.php +++ b/src/GraphQl/Serializer/Exception/HttpExceptionNormalizer.php @@ -51,6 +51,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return $data instanceof Error && $data->getPrevious() instanceof HttpExceptionInterface; } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { return [ diff --git a/src/GraphQl/Serializer/Exception/RuntimeExceptionNormalizer.php b/src/GraphQl/Serializer/Exception/RuntimeExceptionNormalizer.php index cf7e3a449f1..bc12ff5ebdf 100644 --- a/src/GraphQl/Serializer/Exception/RuntimeExceptionNormalizer.php +++ b/src/GraphQl/Serializer/Exception/RuntimeExceptionNormalizer.php @@ -45,6 +45,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return $data instanceof Error && $data->getPrevious() instanceof \RuntimeException; } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { return [ diff --git a/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php b/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php index 5b440aa13ac..b792fb15b89 100644 --- a/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php +++ b/src/GraphQl/Serializer/Exception/ValidationExceptionNormalizer.php @@ -14,7 +14,6 @@ namespace ApiPlatform\GraphQl\Serializer\Exception; use ApiPlatform\Metadata\Exception\RuntimeException; -use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface as LegacyConstraintViolationListAwareExceptionInterface; use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; use GraphQL\Error\Error; use GraphQL\Error\FormattedError; @@ -39,7 +38,7 @@ public function __construct(private readonly array $exceptionToStatus = []) public function normalize(mixed $object, ?string $format = null, array $context = []): array { $validationException = $object->getPrevious(); - if (!($validationException instanceof ConstraintViolationListAwareExceptionInterface || $validationException instanceof LegacyConstraintViolationListAwareExceptionInterface)) { + if (!$validationException instanceof ConstraintViolationListAwareExceptionInterface) { throw new RuntimeException(\sprintf('Object is not a "%s".', ConstraintViolationListAwareExceptionInterface::class)); } @@ -81,6 +80,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return $data instanceof Error && ($data->getPrevious() instanceof ConstraintViolationListAwareExceptionInterface); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { return [ diff --git a/src/GraphQl/Serializer/ItemNormalizer.php b/src/GraphQl/Serializer/ItemNormalizer.php index d0c7c384079..1948733e1d2 100644 --- a/src/GraphQl/Serializer/ItemNormalizer.php +++ b/src/GraphQl/Serializer/ItemNormalizer.php @@ -62,6 +62,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { return self::FORMAT === $format ? parent::getSupportedTypes($format) : []; @@ -89,6 +92,8 @@ public function normalize(mixed $object, ?string $format = null, array $context if ($this->isCacheKeySafe($context)) { $context['cache_key'] = $this->getCacheKey($format, $context); + } else { + $context['cache_key'] = false; } unset($context['operation_name'], $context['operation']); // Remove operation and operation_name only when cache key has been created @@ -147,8 +152,12 @@ protected function getAllowedAttributes(string|object $classOrObject, array $con /** * {@inheritdoc} + * + * @param object $object + * @param string $attribute + * @param string|null $format */ - protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []): void + protected function setAttributeValue($object, $attribute, mixed $value, $format = null, array $context = []): void { if ('_id' === $attribute) { $attribute = 'id'; diff --git a/src/GraphQl/Serializer/ObjectNormalizer.php b/src/GraphQl/Serializer/ObjectNormalizer.php index 03f0272fb77..4d19750a560 100644 --- a/src/GraphQl/Serializer/ObjectNormalizer.php +++ b/src/GraphQl/Serializer/ObjectNormalizer.php @@ -13,22 +13,17 @@ namespace ApiPlatform\GraphQl\Serializer; -use ApiPlatform\Api\IdentifiersExtractorInterface as LegacyIdentifiersExtractorInterface; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Metadata\IdentifiersExtractorInterface; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Exception\UnexpectedValueException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Decorates the output with GraphQL metadata when appropriate, but otherwise just * passes through to the decorated normalizer. */ -final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ObjectNormalizer implements NormalizerInterface { use ClassInfoTrait; @@ -36,7 +31,7 @@ final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMe public const ITEM_RESOURCE_CLASS_KEY = '#itemResourceClass'; public const ITEM_IDENTIFIERS_KEY = '#itemIdentifiers'; - public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly IdentifiersExtractorInterface|LegacyIdentifiersExtractorInterface $identifiersExtractor) + public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface $iriConverter, private readonly IdentifiersExtractorInterface $identifiersExtractor) { } @@ -48,33 +43,12 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && $this->decorated->supportsNormalization($data, $format, $context); } - public function getSupportedTypes($format): array - { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->decorated, 'getSupportedTypes')) { - return [ - '*' => $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(), - ]; - } - - return self::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : []; - } - /** - * {@inheritdoc} + * @param string|null $format */ - public function hasCacheableSupportsMethod(): bool + public function getSupportedTypes($format): array { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(); + return self::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : []; } /** @@ -82,7 +56,7 @@ public function hasCacheableSupportsMethod(): bool * * @throws UnexpectedValueException */ - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $object, ?string $format = null, array $context = []): array { if (isset($context['api_resource'])) { $originalResource = $context['api_resource']; diff --git a/src/GraphQl/State/Processor/NormalizeProcessor.php b/src/GraphQl/State/Processor/NormalizeProcessor.php index 825e562d392..38418295ede 100644 --- a/src/GraphQl/State/Processor/NormalizeProcessor.php +++ b/src/GraphQl/State/Processor/NormalizeProcessor.php @@ -213,6 +213,12 @@ private function serializePageBasedPaginatedCollection(iterable $collection, arr } $data['paginationInfo']['totalCount'] = $collection->getTotalItems(); } + if (isset($selection['paginationInfo']['currentPage'])) { + if (!($collection instanceof PartialPaginatorInterface)) { + throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return currentPage field.', PartialPaginatorInterface::class)); + } + $data['paginationInfo']['currentPage'] = $collection->getCurrentPage(); + } if (isset($selection['paginationInfo']['lastPage'])) { if (!($collection instanceof PaginatorInterface)) { throw new \LogicException(\sprintf('Collection returned by the collection data provider must implement %s to return lastPage field.', PaginatorInterface::class)); diff --git a/src/GraphQl/State/Provider/ReadProvider.php b/src/GraphQl/State/Provider/ReadProvider.php index 58c32f08097..def06700581 100644 --- a/src/GraphQl/State/Provider/ReadProvider.php +++ b/src/GraphQl/State/Provider/ReadProvider.php @@ -82,10 +82,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c return $item; } - if (!\is_object($item)) { - throw new \LogicException('Item from read provider should be a nullable object.'); - } - if (isset($context['graphql_context']) && !enum_exists($item::class)) { $context['graphql_context']['previous_object'] = clone $item; } @@ -124,6 +120,9 @@ private function getNormalizedFilters(array $args): array $filters = $args; foreach ($filters as $name => $value) { + // Prevent numeric keys like `'1'` + $name = (string) $name; + if (\is_array($value)) { if (strpos($name, '_list')) { $name = substr($name, 0, \strlen($name) - \strlen('_list')); @@ -136,7 +135,7 @@ private function getNormalizedFilters(array $args): array $filters[$name] = $this->getNormalizedFilters($value); } - if (\is_string($name) && strpos($name, $this->nestingSeparator)) { + if (strpos($name, $this->nestingSeparator)) { // Gives a chance to relations/nested fields. $index = array_search($name, array_keys($filters), true); $filters = diff --git a/src/GraphQl/Subscription/MercureSubscriptionIriGenerator.php b/src/GraphQl/Subscription/MercureSubscriptionIriGenerator.php index 0069a4a9cc7..d9259a7dc2d 100644 --- a/src/GraphQl/Subscription/MercureSubscriptionIriGenerator.php +++ b/src/GraphQl/Subscription/MercureSubscriptionIriGenerator.php @@ -41,6 +41,6 @@ public function generateTopicIri(string $subscriptionId): string public function generateMercureUrl(string $subscriptionId, ?string $hub = null): string { - return \sprintf('%s?topic=%s', $this->registry->getHub($hub)->getUrl(), $this->generateTopicIri($subscriptionId)); + return \sprintf('%s?topic=%s', $this->registry->getHub($hub)->getPublicUrl(), $this->generateTopicIri($subscriptionId)); } } diff --git a/src/GraphQl/Subscription/SubscriptionManager.php b/src/GraphQl/Subscription/SubscriptionManager.php index 2cd2c2c6873..afebe45bfb1 100644 --- a/src/GraphQl/Subscription/SubscriptionManager.php +++ b/src/GraphQl/Subscription/SubscriptionManager.php @@ -13,7 +13,6 @@ namespace ApiPlatform\GraphQl\Subscription; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; use ApiPlatform\GraphQl\Resolver\Util\IdentifierTrait; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Subscription; @@ -37,7 +36,7 @@ final class SubscriptionManager implements OperationAwareSubscriptionManagerInte use ResourceClassInfoTrait; use SortTrait; - public function __construct(private readonly CacheItemPoolInterface $subscriptionsCache, private readonly SubscriptionIdentifierGeneratorInterface $subscriptionIdentifierGenerator, private readonly ?SerializeStageInterface $serializeStage, private readonly IriConverterInterface $iriConverter, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ?ProcessorInterface $normalizeProcessor = null) + public function __construct(private readonly CacheItemPoolInterface $subscriptionsCache, private readonly SubscriptionIdentifierGeneratorInterface $subscriptionIdentifierGenerator, private readonly ProcessorInterface $normalizeProcessor, private readonly IriConverterInterface $iriConverter, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) { } @@ -83,15 +82,8 @@ public function getPushPayloads(object $object): array $payloads = []; foreach ($subscriptions as [$subscriptionId, $subscriptionFields, $subscriptionResult]) { $resolverContext = ['fields' => $subscriptionFields, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - /** @var Operation */ $operation = (new Subscription())->withName('update_subscription')->withShortName($shortName); - if ($this->normalizeProcessor) { - $data = $this->normalizeProcessor->process($object, $operation, [], $resolverContext); - } elseif ($this->serializeStage) { - $data = ($this->serializeStage)($object, $resourceClass, $operation, $resolverContext); - } else { - throw new \LogicException(); - } + $data = $this->normalizeProcessor->process($object, $operation, [], $resolverContext); unset($data['clientSubscriptionId']); diff --git a/src/GraphQl/Tests/Action/EntrypointActionTest.php b/src/GraphQl/Tests/Action/EntrypointActionTest.php index 14a4d460deb..9fd324313a4 100644 --- a/src/GraphQl/Tests/Action/EntrypointActionTest.php +++ b/src/GraphQl/Tests/Action/EntrypointActionTest.php @@ -15,7 +15,6 @@ use ApiPlatform\GraphQl\Action\EntrypointAction; use ApiPlatform\GraphQl\Action\GraphiQlAction; -use ApiPlatform\GraphQl\Action\GraphQlPlaygroundAction; use ApiPlatform\GraphQl\Error\ErrorHandler; use ApiPlatform\GraphQl\ExecutorInterface; use ApiPlatform\GraphQl\Serializer\Exception\ErrorNormalizer; @@ -92,9 +91,7 @@ public function testPostJsonAction(): void $this->assertEqualsWithoutDateHeader(new JsonResponse(['GraphQL']), $mockedEntrypoint($request)); } - /** - * @dataProvider multipartRequestProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('multipartRequestProvider')] public function testMultipartRequestAction(?string $operations, ?string $map, array $files, array $variables, Response $expectedResponse): void { $requestParams = []; @@ -272,8 +269,7 @@ private function getEntrypointAction(array $variables = ['graphqlVariable']): En $routerProphecy->generate('api_graphql_entrypoint')->willReturn('/graphiql'); $graphiQlAction = new GraphiQlAction($twigProphecy->reveal(), $routerProphecy->reveal(), true); - $graphQlPlaygroundAction = new GraphQlPlaygroundAction($twigProphecy->reveal(), $routerProphecy->reveal(), true); - return new EntrypointAction($schemaBuilderProphecy->reveal(), $executorProphecy->reveal(), $graphiQlAction, $graphQlPlaygroundAction, $normalizer, $errorHandler, false, true, true, 'graphiql'); + return new EntrypointAction($schemaBuilderProphecy->reveal(), $executorProphecy->reveal(), $graphiQlAction, $normalizer, $errorHandler, false, true, 'graphiql'); } } diff --git a/src/GraphQl/Tests/Action/GraphQlPlaygroundActionTest.php b/src/GraphQl/Tests/Action/GraphQlPlaygroundActionTest.php deleted file mode 100644 index 0e8ccd6e143..00000000000 --- a/src/GraphQl/Tests/Action/GraphQlPlaygroundActionTest.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Action; - -use ApiPlatform\GraphQl\Action\GraphQlPlaygroundAction; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; -use Symfony\Component\Routing\RouterInterface; -use Twig\Environment as TwigEnvironment; - -/** - * @author Alan Poulain - */ -class GraphQlPlaygroundActionTest extends TestCase -{ - use ProphecyTrait; - - public function testEnabledAction(): void - { - $request = new Request(); - $mockedAction = $this->getGraphQlPlaygroundAction(true); - - $this->assertInstanceOf(Response::class, $mockedAction($request)); - } - - public function testDisabledAction(): void - { - $request = new Request(); - $mockedAction = $this->getGraphQlPlaygroundAction(false); - - $this->expectExceptionObject(new BadRequestHttpException('GraphQL Playground is not enabled.')); - - $mockedAction($request); - } - - private function getGraphQlPlaygroundAction(bool $enabled): GraphQlPlaygroundAction - { - $twigProphecy = $this->prophesize(TwigEnvironment::class); - $twigProphecy->render(Argument::cetera())->willReturn(''); - $routerProphecy = $this->prophesize(RouterInterface::class); - $routerProphecy->generate('api_graphql_entrypoint')->willReturn('/graphql'); - - return new GraphQlPlaygroundAction($twigProphecy->reveal(), $routerProphecy->reveal(), $enabled, ''); - } -} diff --git a/src/GraphQl/Tests/ExecutorTest.php b/src/GraphQl/Tests/ExecutorTest.php index 43cc420dded..e6b64c77a85 100644 --- a/src/GraphQl/Tests/ExecutorTest.php +++ b/src/GraphQl/Tests/ExecutorTest.php @@ -16,6 +16,8 @@ use ApiPlatform\GraphQl\Executor; use GraphQL\Validator\DocumentValidator; use GraphQL\Validator\Rules\DisableIntrospection; +use GraphQL\Validator\Rules\QueryComplexity; +use GraphQL\Validator\Rules\QueryDepth; use PHPUnit\Framework\TestCase; /** @@ -38,4 +40,20 @@ public function testDisableIntrospectionQuery(): void $expected = new DisableIntrospection(DisableIntrospection::ENABLED); $this->assertEquals($expected, DocumentValidator::getRule(DisableIntrospection::class)); } + + public function testChangeValueOfMaxQueryDepth(): void + { + $executor = new Executor(true, 20); + + $expected = new QueryComplexity(20); + $this->assertEquals($expected, DocumentValidator::getRule(QueryComplexity::class)); + } + + public function testChangeValueOfMaxQueryComplexity(): void + { + $executor = new Executor(true, maxQueryDepth: 20); + + $expected = new QueryDepth(20); + $this->assertEquals($expected, DocumentValidator::getRule(QueryDepth::class)); + } } diff --git a/src/GraphQl/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php b/src/GraphQl/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php index 74ce9c165ae..f4f59f36a55 100644 --- a/src/GraphQl/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php +++ b/src/GraphQl/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php @@ -13,7 +13,6 @@ namespace ApiPlatform\GraphQl\Tests\Fixtures\Serializer\NameConverter; -use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -21,7 +20,7 @@ * Custom converter that will only convert a property named "nameConverted" * with the same logic as Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter. */ -class CustomConverter implements AdvancedNameConverterInterface +class CustomConverter implements NameConverterInterface { private NameConverterInterface $nameConverter; diff --git a/src/GraphQl/Tests/Metadata/RuntimeOperationMetadataFactoryTest.php b/src/GraphQl/Tests/Metadata/RuntimeOperationMetadataFactoryTest.php new file mode 100644 index 00000000000..5dfbfc22351 --- /dev/null +++ b/src/GraphQl/Tests/Metadata/RuntimeOperationMetadataFactoryTest.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\GraphQl\Tests\Metadata; + +use ApiPlatform\GraphQl\Metadata\RuntimeOperationMetadataFactory; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\RouterInterface; + +class RuntimeOperationMetadataFactoryTest extends TestCase +{ + public function testCreate(): void + { + $resourceClass = 'Dummy'; + $operationName = 'item_query'; + + $operation = (new Query())->withName($operationName); + $resourceMetadata = (new ApiResource())->withGraphQlOperations([$operationName => $operation]); + $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass, [$resourceMetadata]); + + $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactory->expects($this->once()) + ->method('create') + ->with($resourceClass) + ->willReturn($resourceMetadataCollection); + + $router = $this->createMock(RouterInterface::class); + $router->expects($this->once()) + ->method('match') + ->with('/dummies/1') + ->willReturn([ + '_api_resource_class' => $resourceClass, + '_api_operation_name' => $operationName, + ]); + + $factory = new RuntimeOperationMetadataFactory($resourceMetadataCollectionFactory, $router); + $this->assertEquals($operation, $factory->create('/dummies/1')); + } + + public function testCreateThrowsExceptionWhenRouteNotFound(): void + { + $this->expectException(\ApiPlatform\Metadata\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('No route matches "/unknown".'); + + $router = $this->createMock(RouterInterface::class); + $router->expects($this->once()) + ->method('match') + ->with('/unknown') + ->willThrowException(new ResourceNotFoundException()); + + $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + + $factory = new RuntimeOperationMetadataFactory($resourceMetadataCollectionFactory, $router); + $factory->create('/unknown'); + } + + public function testCreateThrowsExceptionWhenResourceClassMissing(): void + { + $this->expectException(\ApiPlatform\Metadata\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('The route "/dummies/1" is not an API route, it has no resource class in the defaults.'); + + $router = $this->createMock(RouterInterface::class); + $router->expects($this->once()) + ->method('match') + ->with('/dummies/1') + ->willReturn([]); + + $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + + $factory = new RuntimeOperationMetadataFactory($resourceMetadataCollectionFactory, $router); + $factory->create('/dummies/1'); + } + + public function testCreateThrowsExceptionWhenOperationNotFound(): void + { + $this->expectException(\ApiPlatform\Metadata\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('No operation found for id "/dummies/1".'); + + $resourceClass = 'Dummy'; + + $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactory->expects($this->once()) + ->method('create') + ->with($resourceClass) + ->willReturn(new ResourceMetadataCollection($resourceClass, [new ApiResource()])); + + $router = $this->createMock(RouterInterface::class); + $router->expects($this->once()) + ->method('match') + ->with('/dummies/1') + ->willReturn([ + '_api_resource_class' => $resourceClass, + ]); + + $factory = new RuntimeOperationMetadataFactory($resourceMetadataCollectionFactory, $router); + $factory->create('/dummies/1'); + } + + public function testCreateIgnoresOperationsWithResolvers(): void + { + $this->expectException(\ApiPlatform\Metadata\Exception\InvalidArgumentException::class); + $this->expectExceptionMessage('No operation found for id "/dummies/1".'); + + $resourceClass = 'Dummy'; + $operationName = 'item_query'; + + $operation = (new Query())->withResolver('t')->withName($operationName); + $resourceMetadata = (new ApiResource())->withGraphQlOperations([$operationName => $operation]); + $resourceMetadataCollection = new ResourceMetadataCollection($resourceClass, [$resourceMetadata]); + + $resourceMetadataCollectionFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactory->expects($this->once()) + ->method('create') + ->with($resourceClass) + ->willReturn($resourceMetadataCollection); + + $router = $this->createMock(RouterInterface::class); + $router->expects($this->once()) + ->method('match') + ->with('/dummies/1') + ->willReturn([ + '_api_resource_class' => $resourceClass, + '_api_operation_name' => $operationName, + ]); + + $factory = new RuntimeOperationMetadataFactory($resourceMetadataCollectionFactory, $router); + $factory->create('/dummies/1'); + } +} diff --git a/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php deleted file mode 100644 index 2bb08db8033..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/CollectionResolverFactoryTest.php +++ /dev/null @@ -1,269 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\CollectionResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Tests\Fixtures\Enum\GenderTypeEnum; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; - -/** - * @author Alan Poulain - * @author Kévin Dunglas - */ -class CollectionResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private CollectionResolverFactory $collectionResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $securityPostDenormalizeStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $queryResolverLocatorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->queryResolverLocatorProphecy = $this->prophesize(ContainerInterface::class); - - $this->collectionResolverFactory = new CollectionResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->securityPostDenormalizeStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->queryResolverLocatorProphecy->reveal(), - ); - } - - public function testResolve(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['testField' => 0]; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn(['testField' => true]); - $info = $infoProphecy->reveal(); - $info->fieldName = 'testField'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - 'previous_object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageCollection, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveEnumFieldFromSource(): void - { - $resourceClass = GenderTypeEnum::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['genders' => [GenderTypeEnum::MALE, GenderTypeEnum::FEMALE]]; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'genders'; - - $this->assertSame([GenderTypeEnum::MALE, GenderTypeEnum::FEMALE], ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveFieldNotInSource(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['source']; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn(['testField' => true]); - $info = $infoProphecy->reveal(); - $info->fieldName = 'testField'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldNotBeCalled(); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - 'previous_object' => $readStageCollection, - ], - ])->shouldNotBeCalled(); - - // Null should be returned if the field isn't in the source - as its lack of presence will be due to @ApiProperty security stripping unauthorized fields - $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullSource(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = null; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageCollection, - 'previous_object' => $readStageCollection, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageCollection, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullResourceClass(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['source']; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - - $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullRootClass(): void - { - $resourceClass = \stdClass::class; - $rootClass = null; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = ['source']; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - - $this->assertNull(($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveBadReadStageCollection(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withName($operationName); - $source = null; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Collection from read stage should be iterable.'); - - ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveCustom(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'collection_query'; - $operation = (new QueryCollection())->withResolver('query_resolver_id')->withName($operationName); - $source = null; - $args = ['args']; - $infoProphecy = $this->prophesize(ResolveInfo::class); - $infoProphecy->getFieldSelection()->willReturn([]); - $info = $infoProphecy->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => true, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageCollection = [new \stdClass()]; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageCollection); - - $customCollection = [new \stdClass()]; - $customCollection[0]->field = 'foo'; - $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): array => $customCollection); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customCollection, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customCollection, - 'previous_object' => $customCollection, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($customCollection, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->collectionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ItemMutationResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ItemMutationResolverFactoryTest.php deleted file mode 100644 index c7220499409..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/ItemMutationResolverFactoryTest.php +++ /dev/null @@ -1,321 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\ItemMutationResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\DeserializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\ValidateStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\WriteStageInterface; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; -use ApiPlatform\Metadata\GraphQl\Mutation; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; - -/** - * @author Alan Poulain - */ -class ItemMutationResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private ItemMutationResolverFactory $itemMutationResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $securityPostDenormalizeStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $deserializeStageProphecy; - private ObjectProphecy $writeStageProphecy; - private ObjectProphecy $validateStageProphecy; - private ObjectProphecy $mutationResolverLocatorProphecy; - private ObjectProphecy $securityPostValidationStageProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->deserializeStageProphecy = $this->prophesize(DeserializeStageInterface::class); - $this->writeStageProphecy = $this->prophesize(WriteStageInterface::class); - $this->validateStageProphecy = $this->prophesize(ValidateStageInterface::class); - $this->mutationResolverLocatorProphecy = $this->prophesize(ContainerInterface::class); - $this->securityPostValidationStageProphecy = $this->prophesize(SecurityPostValidationStageInterface::class); - - $this->itemMutationResolverFactory = new ItemMutationResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->securityPostDenormalizeStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->deserializeStageProphecy->reveal(), - $this->writeStageProphecy->reveal(), - $this->validateStageProphecy->reveal(), - $this->mutationResolverLocatorProphecy->reveal(), - $this->securityPostValidationStageProphecy->reveal() - ); - } - - public function testResolve(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = new \stdClass(); - $deserializeStageItem->field = 'deserialize'; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $deserializeStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke($deserializeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled(); - - $writeStageItem = new \stdClass(); - $writeStageItem->field = 'write'; - $this->writeStageProphecy->__invoke($deserializeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($writeStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($writeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullResourceClass(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operation = (new Mutation())->withName('create'); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullOperation(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemMutationResolverFactory)($resourceClass, $rootClass, null)($source, $args, null, $info)); - } - - public function testResolveBadReadStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = []; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Item from read stage should be a nullable object.'); - - ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveNullDeserializeStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = null; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $deserializeStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $this->writeStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $serializeStageData = null; - $this->serializeStageProphecy->__invoke($deserializeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertNull(($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveDelete(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'delete'; - $operation = (new Mutation())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->deserializeStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke(Argument::cetera())->shouldNotBeCalled(); - - $writeStageItem = new \stdClass(); - $writeStageItem->field = 'write'; - $this->writeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($writeStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($writeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveCustom(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withResolver('query_resolver_id')->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = new \stdClass(); - $deserializeStageItem->field = 'deserialize'; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $customItem = new \stdClass(); - $customItem->field = 'foo'; - $this->mutationResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): \stdClass => $customItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $this->validateStageProphecy->__invoke($customItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled(); - - $writeStageItem = new \stdClass(); - $writeStageItem->field = 'write'; - $this->writeStageProphecy->__invoke($customItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($writeStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($writeStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveCustomBadItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'create'; - $operation = (new Mutation())->withResolver('query_resolver_id')->withName($operationName)->withShortName('shortName'); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $readStageItem->field = 'read'; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $deserializeStageItem = new \stdClass(); - $deserializeStageItem->field = 'deserialize'; - $this->deserializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($deserializeStageItem); - - $customItem = new Dummy(); - $this->mutationResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): Dummy => $customItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Custom mutation resolver "query_resolver_id" has to return an item of class shortName but returned an item of class Dummy.'); - - ($this->itemMutationResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php deleted file mode 100644 index 7ab86f63f5f..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/ItemResolverFactoryTest.php +++ /dev/null @@ -1,268 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\ItemResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ChildFoo; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; -use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\ParentFoo; -use ApiPlatform\Metadata\GraphQl\Query; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Psr\Container\ContainerInterface; - -/** - * @author Alan Poulain - * @author Kévin Dunglas - */ -class ItemResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private ItemResolverFactory $itemResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $securityPostDenormalizeStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $queryResolverLocatorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->securityPostDenormalizeStageProphecy = $this->prophesize(SecurityPostDenormalizeStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->queryResolverLocatorProphecy = $this->prophesize(ContainerInterface::class); - - $this->itemResolverFactory = new ItemResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->securityPostDenormalizeStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->queryResolverLocatorProphecy->reveal() - ); - } - - /** - * @dataProvider itemResourceProvider - */ - public function testResolve(?string $resourceClass, string $determinedResourceClass, ?object $readStageItem): void - { - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->securityStageProphecy->__invoke($determinedResourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($determinedResourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - 'previous_object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $determinedResourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public static function itemResourceProvider(): array - { - return [ - 'nominal' => [\stdClass::class, \stdClass::class, new \stdClass()], - 'null item' => [\stdClass::class, \stdClass::class, null], - 'null resource class' => [null, \stdClass::class, new \stdClass()], - ]; - } - - public function testResolveNested(): void - { - $source = ['nested' => ['already_serialized']]; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'nested'; - - $this->assertEquals(['already_serialized'], ($this->itemResolverFactory)('resourceClass')($source, [], null, $info)); - } - - public function testResolveNestedNullValue(): void - { - $source = ['nestedNullValue' => null]; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'nestedNullValue'; - - $this->assertNull(($this->itemResolverFactory)('resourceClass')($source, [], null, $info)); - } - - public function testResolveBadReadStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = []; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Item from read stage should be a nullable object.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveNoResourceNoItem(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = null; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Resource class cannot be determined.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveBadItem(): void - { - $resourceClass = Dummy::class; - $rootClass = 'rootClass'; - $operationName = 'item_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Resolver only handles items of class Dummy but retrieved item is of class stdClass.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveCustom(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'custom_query'; - $operation = (new Query())->withResolver('query_resolver_id')->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $customItem = new \stdClass(); - $customItem->field = 'foo'; - $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): \stdClass => $customItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customItem, - ], - ])->shouldBeCalled(); - $this->securityPostDenormalizeStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $customItem, - 'previous_object' => $customItem, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($customItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $this->assertSame($serializeStageData, ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveCustomBadItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'custom_query'; - $operation = (new Query())->withResolver('query_resolver_id')->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $customItem = new Dummy(); - $this->queryResolverLocatorProphecy->get('query_resolver_id')->shouldBeCalled()->willReturn(fn (): Dummy => $customItem); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Custom query resolver "query_resolver_id" has to return an item of class stdClass but returned an item of class Dummy.'); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveInheritedClass(): void - { - $resourceClass = ParentFoo::class; - $rootClass = $resourceClass; - $operationName = 'custom_query'; - $operation = (new Query())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $info->fieldName = 'field'; - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => false]; - - $readStageItem = new ChildFoo(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - ($this->itemResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ItemSubscriptionResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ItemSubscriptionResolverFactoryTest.php deleted file mode 100644 index 5d455a6df0f..00000000000 --- a/src/GraphQl/Tests/Resolver/Factory/ItemSubscriptionResolverFactoryTest.php +++ /dev/null @@ -1,197 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Factory; - -use ApiPlatform\GraphQl\Resolver\Factory\ItemSubscriptionResolverFactory; -use ApiPlatform\GraphQl\Resolver\Stage\ReadStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStageInterface; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; -use ApiPlatform\GraphQl\Subscription\MercureSubscriptionIriGeneratorInterface; -use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Subscription; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; - -/** - * @author Alan Poulain - */ -class ItemSubscriptionResolverFactoryTest extends TestCase -{ - use ProphecyTrait; - - private ItemSubscriptionResolverFactory $itemSubscriptionResolverFactory; - private ObjectProphecy $readStageProphecy; - private ObjectProphecy $securityStageProphecy; - private ObjectProphecy $serializeStageProphecy; - private ObjectProphecy $subscriptionManagerProphecy; - private ObjectProphecy $mercureSubscriptionIriGeneratorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->readStageProphecy = $this->prophesize(ReadStageInterface::class); - $this->securityStageProphecy = $this->prophesize(SecurityStageInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); - $this->subscriptionManagerProphecy = $this->prophesize(SubscriptionManagerInterface::class); - $this->mercureSubscriptionIriGeneratorProphecy = $this->prophesize(MercureSubscriptionIriGeneratorInterface::class); - - $this->itemSubscriptionResolverFactory = new ItemSubscriptionResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->subscriptionManagerProphecy->reveal(), - $this->mercureSubscriptionIriGeneratorProphecy->reveal() - ); - } - - public function testResolve(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withMercure(true)->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->securityStageProphecy->__invoke($resourceClass, $operation, $resolverContext + [ - 'extra_variables' => [ - 'object' => $readStageItem, - ], - ])->shouldBeCalled(); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($serializeStageData); - - $subscriptionId = 'subscriptionId'; - $this->subscriptionManagerProphecy->retrieveSubscriptionId($resolverContext, $serializeStageData)->shouldBeCalled()->willReturn($subscriptionId); - - $mercureUrl = 'mercure-url'; - $this->mercureSubscriptionIriGeneratorProphecy->generateMercureUrl($subscriptionId, null)->shouldBeCalled()->willReturn($mercureUrl); - - $this->assertSame($serializeStageData + ['mercureUrl' => $mercureUrl], ($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullResourceClass(): void - { - $resourceClass = null; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNullOperationName(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->assertNull(($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, null)($source, $args, null, $info)); - } - - public function testResolveBadReadStageItem(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withName($operationName); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $readStageItem = []; - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->shouldBeCalled()->willReturn($readStageItem); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Item from read stage should be a nullable object.'); - - ($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } - - public function testResolveNoSubscriptionId(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - $operation = (new Subscription())->withName($operationName)->withMercure(true); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->willReturn($readStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->willReturn($serializeStageData); - - $this->subscriptionManagerProphecy->retrieveSubscriptionId($resolverContext, $serializeStageData)->willReturn(null); - - $this->mercureSubscriptionIriGeneratorProphecy->generateMercureUrl(Argument::any())->shouldNotBeCalled(); - - $this->assertSame($serializeStageData, ($this->itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info)); - } - - public function testResolveNoMercureSubscriptionIriGenerator(): void - { - $resourceClass = \stdClass::class; - $rootClass = 'rootClass'; - $operationName = 'update'; - /** @var Operation $operation */ - $operation = (new Subscription())->withName($operationName)->withMercure(true); - $source = ['source']; - $args = ['args']; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $resolverContext = ['source' => $source, 'args' => $args, 'info' => $info, 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]; - - $readStageItem = new \stdClass(); - $this->readStageProphecy->__invoke($resourceClass, $rootClass, $operation, $resolverContext)->willReturn($readStageItem); - - $serializeStageData = ['serialized']; - $this->serializeStageProphecy->__invoke($readStageItem, $resourceClass, $operation, $resolverContext)->willReturn($serializeStageData); - - $subscriptionId = 'subscriptionId'; - $this->subscriptionManagerProphecy->retrieveSubscriptionId($resolverContext, $serializeStageData)->willReturn($subscriptionId); - - $this->expectException(\LogicException::class); - $this->expectExceptionMessage('Cannot use Mercure for subscriptions when MercureBundle is not installed. Try running "composer require mercure".'); - - $itemSubscriptionResolverFactory = new ItemSubscriptionResolverFactory( - $this->readStageProphecy->reveal(), - $this->securityStageProphecy->reveal(), - $this->serializeStageProphecy->reveal(), - $this->subscriptionManagerProphecy->reveal(), - null - ); - - ($itemSubscriptionResolverFactory)($resourceClass, $rootClass, $operation)($source, $args, null, $info); - } -} diff --git a/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php b/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php index ee0be4aef60..a2ba930bb81 100644 --- a/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php +++ b/src/GraphQl/Tests/Resolver/Factory/ResolverFactoryTest.php @@ -18,6 +18,7 @@ use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\State\ProcessorInterface; use ApiPlatform\State\ProviderInterface; @@ -26,9 +27,7 @@ class ResolverFactoryTest extends TestCase { - /** - * @dataProvider graphQlQueries - */ + #[\PHPUnit\Framework\Attributes\DataProvider('graphQlQueries')] public function testGraphQlResolver(?string $resourceClass = null, ?string $rootClass = null, ?Operation $operation = null, ?Operation $providedOperation = null, ?Operation $processedOperation = null): void { $returnValue = new \stdClass(); @@ -45,7 +44,7 @@ public function testGraphQlResolver(?string $resourceClass = null, ?string $root $resolveInfo = $this->createMock(ResolveInfo::class); $resolveInfo->fieldName = 'test'; - $resolverFactory = new ResolverFactory($provider, $processor); + $resolverFactory = new ResolverFactory($provider, $processor, $this->createMock(OperationMetadataFactoryInterface::class)); $this->assertEquals($resolverFactory->__invoke($resourceClass, $rootClass, $operation, $propertyMetadataFactory)(['test' => null], [], [], $resolveInfo), $returnValue); } @@ -56,4 +55,21 @@ public static function graphQlQueries(): array ['Dummy', 'Dummy', new Mutation(), (new Mutation())->withValidate(true), (new Mutation())->withValidate(true)->withWrite(true)], ]; } + + public function testGraphQlResolverWithNode(): void + { + $returnValue = new \stdClass(); + $op = new Query(name: 'hi'); + $provider = $this->createMock(ProviderInterface::class); + $provider->expects($this->once())->method('provide')->with($op)->willReturn($returnValue); + $processor = $this->createMock(ProcessorInterface::class); + $processor->expects($this->once())->method('process')->with($returnValue, $op)->willReturn($returnValue); + $resolveInfo = $this->createMock(ResolveInfo::class); + $resolveInfo->fieldName = 'test'; + + $operationFactory = $this->createMock(OperationMetadataFactoryInterface::class); + $operationFactory->method('create')->with('/foo')->willReturn($op); + $resolverFactory = new ResolverFactory($provider, $processor, $operationFactory); + $this->assertSame($returnValue, $resolverFactory->__invoke()([], ['id' => '/foo'], [], $resolveInfo)); + } } diff --git a/src/GraphQl/Tests/Resolver/Stage/DeserializeStageTest.php b/src/GraphQl/Tests/Resolver/Stage/DeserializeStageTest.php deleted file mode 100644 index 174dbe06f92..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/DeserializeStageTest.php +++ /dev/null @@ -1,92 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\DeserializeStage; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; - -/** - * @author Alan Poulain - */ -class DeserializeStageTest extends TestCase -{ - use ProphecyTrait; - - private DeserializeStage $deserializeStage; - private ObjectProphecy $denormalizerProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->denormalizerProphecy = $this->prophesize(DenormalizerInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $this->deserializeStage = new DeserializeStage( - $this->denormalizerProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal() - ); - } - - /** - * @dataProvider objectToPopulateProvider - */ - public function testApplyDisabled(?object $objectToPopulate): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query')->withClass($resourceClass)->withDeserialize(false); - $result = ($this->deserializeStage)($objectToPopulate, $resourceClass, $operation, []); - - $this->assertSame($objectToPopulate, $result); - } - - /** - * @dataProvider objectToPopulateProvider - */ - public function testApply(?object $objectToPopulate, array $denormalizationContext): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - $context = ['args' => ['input' => 'myInput']]; - - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, false)->shouldBeCalled()->willReturn($denormalizationContext); - - $denormalizedData = new \stdClass(); - $this->denormalizerProphecy->denormalize($context['args']['input'], $resourceClass, ItemNormalizer::FORMAT, $denormalizationContext)->shouldBeCalled()->willReturn($denormalizedData); - - $result = ($this->deserializeStage)($objectToPopulate, $resourceClass, $operation, $context); - - $this->assertSame($denormalizedData, $result); - } - - public static function objectToPopulateProvider(): array - { - return [ - 'null' => [null, ['denormalization' => true]], - 'object' => [$object = new \stdClass(), ['denormalization' => true, ItemNormalizer::OBJECT_TO_POPULATE => $object]], - ]; - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/ReadStageTest.php b/src/GraphQl/Tests/Resolver/Stage/ReadStageTest.php deleted file mode 100644 index 40c6c37a03f..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/ReadStageTest.php +++ /dev/null @@ -1,270 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\ReadStage; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\Exception\ItemNotFoundException; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\State\ProviderInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; - -/** - * @author Alan Poulain - */ -class ReadStageTest extends TestCase -{ - use ProphecyTrait; - - private ReadStage $readStage; - private ObjectProphecy $iriConverterProphecy; - private ObjectProphecy $providerProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $this->providerProphecy = $this->prophesize(ProviderInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $this->readStage = new ReadStage( - $this->iriConverterProphecy->reveal(), - $this->providerProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal(), - '_' - ); - } - - /** - * @dataProvider contextProvider - */ - public function testApplyDisabled(array $context, object|array|null $expectedResult): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withRead(false)->withName('item_query')->withClass($resourceClass); - - $result = ($this->readStage)($resourceClass, null, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function contextProvider(): array - { - return [ - 'item context' => [['is_collection' => false], null], - 'collection context' => [['is_collection' => true], []], - ]; - } - - /** - * @dataProvider itemProvider - */ - public function testApplyItem(?string $identifier, ?object $item, bool $throwNotFound, ?object $expectedResult): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $context = [ - 'is_collection' => false, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => ['id' => $identifier], - 'info' => $info, - ]; - - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName); - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - if ($throwNotFound) { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willThrow(new ItemNotFoundException()); - } else { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willReturn($item); - } - - $result = ($this->readStage)($resourceClass, null, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function itemProvider(): array - { - $item = new \stdClass(); - - return [ - 'no identifier' => [null, $item, false, null], - 'identifier' => ['identifier', $item, false, $item], - 'identifier not found' => ['identifier_not_found', $item, true, null], - ]; - } - - /** - * @dataProvider itemMutationOrSubscriptionProvider - */ - public function testApplyMutationOrSubscription(bool $isMutation, bool $isSubscription, string $resourceClass, ?string $identifier, ?object $item, bool $throwNotFound, ?object $expectedResult, ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void - { - $operationName = 'create'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $context = [ - 'is_collection' => false, - 'is_mutation' => $isMutation, - 'is_subscription' => $isSubscription, - 'args' => ['input' => ['id' => $identifier]], - 'info' => $info, - ]; - - /** @var Operation $operation */ - $operation = (new Mutation())->withName($operationName)->withShortName('shortName'); - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - if ($throwNotFound) { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willThrow(new ItemNotFoundException()); - } else { - $this->iriConverterProphecy->getResourceFromIri($identifier, $normalizationContext)->willReturn($item); - } - - if ($expectedExceptionClass) { - $this->expectException($expectedExceptionClass); - $this->expectExceptionMessage($expectedExceptionMessage); - } - - $result = ($this->readStage)($resourceClass, null, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function itemMutationOrSubscriptionProvider(): array - { - $item = new \stdClass(); - - return [ - 'no identifier' => [true, false, 'myResource', null, $item, false, null], - 'identifier' => [true, false, \stdClass::class, 'identifier', $item, false, $item], - 'identifier bad item' => [true, false, 'myResource', 'identifier', $item, false, $item, \UnexpectedValueException::class, 'Item "identifier" did not match expected type "shortName".'], - 'identifier not found' => [true, false, 'myResource', 'identifier_not_found', $item, true, null, NotFoundHttpException::class, 'Item "identifier_not_found" not found.'], - 'no identifier (subscription)' => [false, true, 'myResource', null, $item, false, null], - 'identifier (subscription)' => [false, true, \stdClass::class, 'identifier', $item, false, $item], - ]; - } - - /** - * @dataProvider collectionProvider - */ - public function testApplyCollection(array $args, ?string $rootClass, ?array $source, array $expectedFilters, iterable $expectedResult): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $fieldName = 'resource'; - $info->fieldName = $fieldName; - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => $args, - 'info' => $info, - 'source' => $source, - ]; - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withName($operationName); - $normalizationContext = ['normalization' => true, 'operation' => $operation]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->providerProphecy->provide($operation, [], $normalizationContext + ['filters' => $expectedFilters])->willReturn([]); - $this->providerProphecy->provide($operation, ['id' => 3], $normalizationContext + ['filters' => $expectedFilters, 'linkClass' => 'myResource', 'linkProperty' => 'resource'])->willReturn(['resource']); - - $result = ($this->readStage)($resourceClass, $rootClass, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public function testPreserveOrderOfOrderFiltersIfNested(): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $fieldName = 'resource'; - $info->fieldName = $fieldName; - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => [ - 'order' => [ - 'some_field' => 'ASC', - 'localField' => 'ASC', - ], - ], - 'info' => $info, - 'source' => null, - ]; - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withName($operationName); - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - ($this->readStage)($resourceClass, $resourceClass, $operation, $context); - - $this->providerProphecy->provide($operation, [], Argument::that(fn ($args): bool => // Prophecy does not check the order of items in associative arrays. Checking if some.field comes first manually -array_search('some.field', array_keys($args['filters']['order']), true) < - array_search('localField', array_keys($args['filters']['order']), true)))->shouldHaveBeenCalled(); - } - - public static function collectionProvider(): array - { - return [ - 'no root class' => [[], null, null, [], []], - 'nominal' => [ - ['filter_list' => 'filtered', 'filter_field_list' => ['filtered1', 'filtered2']], - 'myResource', - null, - ['filter_list' => 'filtered', 'filter_field_list' => ['filtered1', 'filtered2'], 'filter.list' => 'filtered', 'filter_field' => ['filtered1', 'filtered2'], 'filter.field' => ['filtered1', 'filtered2']], - [], - ], - 'with array filter syntax' => [ - ['filter' => [['filterArg1' => 'filterValue1'], ['filterArg2' => 'filterValue2']]], - 'myResource', - null, - ['filter' => ['filterArg1' => 'filterValue1', 'filterArg2' => 'filterValue2']], - [], - ], - 'with resource' => [ - [], - 'myResource', - ['resource' => [], ItemNormalizer::ITEM_IDENTIFIERS_KEY => ['id' => 3], ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => 'myResource'], - [], - ['resource'], - ], - ]; - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SecurityPostDenormalizeStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SecurityPostDenormalizeStageTest.php deleted file mode 100644 index 1b870ea2b57..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SecurityPostDenormalizeStageTest.php +++ /dev/null @@ -1,124 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostDenormalizeStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * @author Alan Poulain - * @author Vincent Chalamon - */ -class SecurityPostDenormalizeStageTest extends TestCase -{ - use ProphecyTrait; - - private SecurityPostDenormalizeStage $securityPostDenormalizeStage; - private ObjectProphecy $resourceAccessCheckerProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - - $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage( - $this->resourceAccessCheckerProphecy->reveal() - ); - } - - public function testNoSecurity(): void - { - $resourceClass = 'myResource'; - $operation = new Query(); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::cetera())->shouldNotBeCalled(); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, []); - } - - public function testGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withSecurityPostDenormalize($isGranted)->withName($operationName); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(true); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, ['extra_variables' => $extraVariables]); - } - - public function testNotGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withSecurityPostDenormalize($isGranted)->withName($operationName); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(false); - - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->expectException(AccessDeniedHttpException::class); - $this->expectExceptionMessage('Access Denied.'); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, [ - 'info' => $info, - 'extra_variables' => $extraVariables, - ]); - } - - public function testNoSecurityBundleInstalled(): void - { - $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - /** @var Operation $operation */ - $operation = (new Query())->withSecurityPostDenormalize($isGranted)->withName($operationName); - - $this->expectException(\LogicException::class); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, []); - } - - public function testNoSecurityBundleInstalledNoExpression(): void - { - $this->securityPostDenormalizeStage = new SecurityPostDenormalizeStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); - - ($this->securityPostDenormalizeStage)($resourceClass, $operation, []); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SecurityPostValidationStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SecurityPostValidationStageTest.php deleted file mode 100644 index d824578b9d9..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SecurityPostValidationStageTest.php +++ /dev/null @@ -1,127 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SecurityPostValidationStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * @author Alan Poulain - * @author Vincent Chalamon - */ -class SecurityPostValidationStageTest extends TestCase -{ - use ProphecyTrait; - - private SecurityPostValidationStage $securityPostValidationStage; - private ObjectProphecy $resourceAccessCheckerProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - - $this->securityPostValidationStage = new SecurityPostValidationStage( - $this->resourceAccessCheckerProphecy->reveal() - ); - } - - public function testNoSecurity(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::cetera())->shouldNotBeCalled(); - - ($this->securityPostValidationStage)($resourceClass, $operation, []); - } - - public function testGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurityPostValidation($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(true); - - ($this->securityPostValidationStage)($resourceClass, $operation, ['extra_variables' => $extraVariables]); - } - - public function testNotGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurityPostValidation($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(false); - - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->expectException(AccessDeniedHttpException::class); - $this->expectExceptionMessage('Access Denied.'); - - ($this->securityPostValidationStage)($resourceClass, $operation, [ - 'info' => $info, - 'extra_variables' => $extraVariables, - ]); - } - - public function testNoSecurityBundleInstalled(): void - { - $this->securityPostValidationStage = new SecurityPostValidationStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurityPostValidation($isGranted); - - $this->expectException(\LogicException::class); - - ($this->securityPostValidationStage)($resourceClass, $operation, []); - } - - public function testNoSecurityBundleInstalledNoExpression(): void - { - $this->securityPostValidationStage = new SecurityPostValidationStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); - - ($this->securityPostValidationStage)($resourceClass, $operation, []); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SecurityStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SecurityStageTest.php deleted file mode 100644 index 1eb69316e4e..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SecurityStageTest.php +++ /dev/null @@ -1,122 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SecurityStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\ResourceAccessCheckerInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -/** - * @author Alan Poulain - * @author Vincent Chalamon - */ -class SecurityStageTest extends TestCase -{ - use ProphecyTrait; - - private SecurityStage $securityStage; - private ObjectProphecy $resourceAccessCheckerProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->resourceAccessCheckerProphecy = $this->prophesize(ResourceAccessCheckerInterface::class); - - $this->securityStage = new SecurityStage( - $this->resourceAccessCheckerProphecy->reveal() - ); - } - - public function testNoSecurity(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass); - - $this->resourceAccessCheckerProphecy->isGranted(Argument::cetera())->shouldNotBeCalled(); - - ($this->securityStage)($resourceClass, $operation, []); - } - - public function testGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurity($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(true); - - ($this->securityStage)($resourceClass, $operation, ['extra_variables' => $extraVariables]); - } - - public function testNotGranted(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - $extraVariables = ['extra' => false]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurity($isGranted); - - $this->resourceAccessCheckerProphecy->isGranted($resourceClass, $isGranted, $extraVariables)->shouldBeCalled()->willReturn(false); - - $info = $this->prophesize(ResolveInfo::class)->reveal(); - - $this->expectException(AccessDeniedHttpException::class); - $this->expectExceptionMessage('Access Denied.'); - - ($this->securityStage)($resourceClass, $operation, [ - 'info' => $info, - 'extra_variables' => $extraVariables, - ]); - } - - public function testNoSecurityBundleInstalled(): void - { - $this->securityStage = new SecurityStage(null); - - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $isGranted = 'not_granted'; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName)->withClass($resourceClass)->withSecurity($isGranted); - - $this->expectException(\LogicException::class); - - ($this->securityStage)($resourceClass, $operation, []); - } - - public function testNoSecurityBundleInstalledNoExpression(): void - { - $this->securityStage = new SecurityStage(null); - - $resourceClass = 'myResource'; - $this->resourceAccessCheckerProphecy->isGranted(Argument::any())->shouldNotBeCalled(); - - ($this->securityStage)($resourceClass, new Query(), []); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php b/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php deleted file mode 100644 index afcf85ffbef..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/SerializeStageTest.php +++ /dev/null @@ -1,287 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStage; -use ApiPlatform\GraphQl\Serializer\ItemNormalizer; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\GraphQl\QueryCollection; -use ApiPlatform\Metadata\GraphQl\Subscription; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\State\Pagination\ArrayPaginator; -use ApiPlatform\State\Pagination\Pagination; -use ApiPlatform\State\Pagination\PaginatorInterface; -use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; - -/** - * @author Alan Poulain - */ -class SerializeStageTest extends TestCase -{ - use ProphecyTrait; - - private ObjectProphecy $normalizerProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - private ObjectProphecy $resolveInfoProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->normalizerProphecy = $this->prophesize(NormalizerInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - $this->resolveInfoProphecy = $this->prophesize(ResolveInfo::class); - } - - /** - * @dataProvider applyDisabledProvider - */ - public function testApplyDisabled(Operation $operation, bool $paginationEnabled, ?array $expectedResult): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = $operation->withSerialize(false); - - $result = ($this->createSerializeStage($paginationEnabled))(null, $resourceClass, $operation, []); - - $this->assertSame($expectedResult, $result); - } - - public static function applyDisabledProvider(): array - { - return [ - 'item' => [new Query(), false, null], - 'collection with pagination' => [new QueryCollection(), true, ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]]], - 'collection without pagination' => [new QueryCollection(), false, []], - 'mutation' => [new Mutation(), false, ['clientMutationId' => null]], - 'subscription' => [new Subscription(), false, ['clientSubscriptionId' => null]], - ]; - } - - /** - * @dataProvider applyProvider - */ - public function testApply(object|array $itemOrCollection, string $operationName, callable $contextFactory, bool $paginationEnabled, ?array $expectedResult): void - { - $context = $contextFactory($this); - - $resourceClass = 'myResource'; - $operation = $context['is_mutation'] ? new Mutation() : new Query(); - if ($context['is_subscription']) { - $operation = new Subscription(); - } - - if ($context['is_collection'] ?? false) { - $operation = new QueryCollection(); - } - - /** @var Operation $operation */ - $operation = $operation->withShortName('shortName')->withName($operationName)->withClass($resourceClass); - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(['normalized_item']); - - $result = ($this->createSerializeStage($paginationEnabled))($itemOrCollection, $resourceClass, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function applyProvider(): iterable - { - $defaultContextFactory = fn (self $that): array => [ - 'args' => [], - 'info' => $that->resolveInfoProphecy->reveal(), - ]; - - yield 'item' => [new \stdClass(), 'item_query', fn (self $that): array => $defaultContextFactory($that) + ['is_collection' => false, 'is_mutation' => false, 'is_subscription' => false], false, ['normalized_item']]; - yield 'collection without pagination' => [[new \stdClass(), new \stdClass()], 'collection_query', fn (self $that): array => $defaultContextFactory($that) + ['is_collection' => true, 'is_mutation' => false, 'is_subscription' => false], false, [['normalized_item'], ['normalized_item']]]; - yield 'mutation' => [new \stdClass(), 'create', fn (self $that): array => array_merge($defaultContextFactory($that), ['args' => ['input' => ['clientMutationId' => 'clientMutationId']], 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]), false, ['shortName' => ['normalized_item'], 'clientMutationId' => 'clientMutationId']]; - yield 'delete mutation' => [new \stdClass(), 'delete', fn (self $that): array => array_merge($defaultContextFactory($that), ['args' => ['input' => ['id' => '/iri/4']], 'is_collection' => false, 'is_mutation' => true, 'is_subscription' => false]), false, ['shortName' => ['id' => '/iri/4'], 'clientMutationId' => null]]; - yield 'subscription' => [new \stdClass(), 'update', fn (self $that): array => array_merge($defaultContextFactory($that), ['args' => ['input' => ['clientSubscriptionId' => 'clientSubscriptionId']], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true]), false, ['shortName' => ['normalized_item'], 'clientSubscriptionId' => 'clientSubscriptionId']]; - } - - /** - * @dataProvider applyCollectionWithPaginationProvider - */ - public function testApplyCollectionWithPagination(iterable|callable $collection, array $args, ?array $expectedResult, bool $pageBasedPagination, array $getFieldSelection = [], ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => $args, - 'info' => $this->resolveInfoProphecy->reveal(), - ]; - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withShortName('shortName')->withName($operationName); - if ($pageBasedPagination) { - $operation = $operation->withPaginationType('page'); - } - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(['normalized_item']); - - if ($expectedExceptionClass) { - $this->expectException($expectedExceptionClass); - $this->expectExceptionMessage($expectedExceptionMessage); - } - - $result = ($this->createSerializeStage(true))(\is_callable($collection) ? $collection($this) : $collection, $resourceClass, $operation, $context); - - $this->assertSame($expectedResult, $result); - } - - public static function applyCollectionWithPaginationProvider(): iterable - { - $partialPaginatorFactory = function (self $that): PartialPaginatorInterface { - $partialPaginatorProphecy = $that->prophesize(PartialPaginatorInterface::class); - $partialPaginatorProphecy->count()->willReturn(2); - $partialPaginatorProphecy->valid()->willReturn(false); - $partialPaginatorProphecy->getItemsPerPage()->willReturn(2.0); - $partialPaginatorProphecy->rewind(); - - return $partialPaginatorProphecy->reveal(); - }; - - yield 'cursor - not paginator' => [[], [], null, false, [], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface or ApiPlatform\State\Pagination\PartialPaginatorInterface.']; - yield 'cursor - empty paginator' => [new ArrayPaginator([], 0, 0), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => [], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MA=='], ['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with after cursor' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 2), ['after' => 'MA=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with bad after cursor' => [new ArrayPaginator([], 0, 0), ['after' => '-'], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; - yield 'cursor - paginator with empty after cursor' => [new ArrayPaginator([], 0, 0), ['after' => ''], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; - yield 'cursor - paginator with before cursor' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 1), ['before' => 'Mg=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with bad before cursor' => [new ArrayPaginator([], 0, 0), ['before' => '-'], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; - yield 'cursor - paginator with empty before cursor' => [new ArrayPaginator([], 0, 0), ['before' => ''], null, false, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; - yield 'cursor - paginator with last' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 1, 2), ['last' => 2], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - partial paginator' => [$partialPaginatorFactory, [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => false, 'hasPreviousPage' => false]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - partial paginator with after cursor' => [$partialPaginatorFactory, ['after' => 'MA=='], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], false, ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - - yield 'page - not paginator, itemsPerPage requested' => [[], [], null, true, ['paginationInfo' => ['itemsPerPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PartialPaginatorInterface to return itemsPerPage field.']; - yield 'page - not paginator, lastPage requested' => [[], [], null, true, ['paginationInfo' => ['lastPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return lastPage field.']; - yield 'page - not paginator, totalCount requested' => [[], [], null, true, ['paginationInfo' => ['totalCount' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return totalCount field.']; - yield 'page - not paginator, hasNextPage requested' => [[], [], null, true, ['paginationInfo' => ['hasNextPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\HasNextPagePaginatorInterface to return hasNextPage field.']; - yield 'page - empty paginator - itemsPerPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['itemsPerPage' => .0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - empty paginator - lastPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['lastPage' => 1.0]], true, ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - empty paginator - totalCount requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['totalCount' => .0]], true, ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - empty paginator - hasNextPage requested' => [new ArrayPaginator([], 0, 0), [], ['collection' => [], 'paginationInfo' => ['hasNextPage' => false]], true, ['paginationInfo' => ['hasNextPage' => true]]]; - yield 'page - paginator page 1 - itemsPerPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - paginator page 1 - lastPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], true, ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - paginator page 1 - totalCount requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], true, ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - paginator page 1 - hasNextPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 0, 2), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['hasNextPage' => true]], true, ['paginationInfo' => ['hasNextPage' => true]]]; - yield 'page - paginator with page - itemsPerPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], true, ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - paginator with page - lastPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], true, ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - paginator with page - totalCount requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], true, ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - paginator with page - hasNextPage requested' => [new ArrayPaginator([new \stdClass(), new \stdClass(), new \stdClass()], 2, 2), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['hasNextPage' => false]], true, ['paginationInfo' => ['hasNextPage' => true]]]; - } - - /** - * @dataProvider applyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequestedProvider - */ - public function testApplyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequested(bool $pageBasedPagination, array $getFieldSelection = [], bool $getTotalItemsCalled = false): void - { - $operationName = 'collection_query'; - $resourceClass = 'myResource'; - $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); - $context = [ - 'is_collection' => true, - 'is_mutation' => false, - 'is_subscription' => false, - 'args' => [], - 'info' => $this->resolveInfoProphecy->reveal(), - ]; - $collectionProphecy = $this->prophesize(PaginatorInterface::class); - $collectionProphecy->getTotalItems()->willReturn(1); - $collectionProphecy->count()->willReturn(1); - $collectionProphecy->getItemsPerPage()->willReturn(20.0); - $collectionProphecy->valid()->willReturn(false); - $collectionProphecy->rewind(); - if ($getTotalItemsCalled) { - $collectionProphecy->getTotalItems()->shouldBeCalledOnce(); - } else { - $collectionProphecy->getTotalItems()->shouldNotBeCalled(); - } - - /** @var Operation $operation */ - $operation = (new QueryCollection())->withShortName('shortName')->withName($operationName); - if ($pageBasedPagination) { - $operation = $operation->withPaginationType('page'); - } - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(['normalized_item']); - - ($this->createSerializeStage(true))($collectionProphecy->reveal(), $resourceClass, $operation, $context); - } - - public static function applyCollectionWithPaginationShouldNotCountItemsUnlessFieldRequestedProvider(): iterable - { - yield 'cursor - totalCount requested' => [false, ['totalCount' => true], true]; - yield 'cursor - totalCount not requested' => [false, [], false]; - yield 'page - totalCount requested' => [true, ['paginationInfo' => ['totalCount' => true]], true]; - yield 'page - totalCount not requested' => [true, [], false]; - } - - public function testApplyBadNormalizedData(): void - { - $operationName = 'item_query'; - $resourceClass = 'myResource'; - $context = ['is_collection' => false, 'is_mutation' => false, 'is_subscription' => false, 'args' => [], 'info' => $this->prophesize(ResolveInfo::class)->reveal()]; - /** @var Operation $operation */ - $operation = (new Query())->withName($operationName); - - $normalizationContext = ['normalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(0); - - $this->expectException(\UnexpectedValueException::class); - $this->expectExceptionMessage('Expected serialized data to be a nullable array.'); - - ($this->createSerializeStage(false))(new \stdClass(), $resourceClass, $operation, $context); - } - - private function createSerializeStage(bool $paginationEnabled): SerializeStage - { - $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $resourceMetadataCollectionFactoryProphecy->create(Argument::type('string'))->willReturn(new ResourceMetadataCollection('')); - $pagination = new Pagination([], ['enabled' => $paginationEnabled]); - - return new SerializeStage( - $this->normalizerProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal(), - $pagination - ); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/ValidateStageTest.php b/src/GraphQl/Tests/Resolver/Stage/ValidateStageTest.php deleted file mode 100644 index 41c683f3d8c..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/ValidateStageTest.php +++ /dev/null @@ -1,90 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\ValidateStage; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Validator\Exception\ValidationException; -use ApiPlatform\Validator\ValidatorInterface; -use GraphQL\Type\Definition\ResolveInfo; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\Validator\ConstraintViolationList; - -/** - * @author Alan Poulain - */ -class ValidateStageTest extends TestCase -{ - use ProphecyTrait; - - private ValidateStage $validateStage; - private ObjectProphecy $validatorProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->validatorProphecy = $this->prophesize(ValidatorInterface::class); - - $this->validateStage = new ValidateStage( - $this->validatorProphecy->reveal() - ); - } - - public function testApplyDisabled(): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withValidate(false)->withName('item_query'); - - $this->validatorProphecy->validate(Argument::cetera())->shouldNotBeCalled(); - - ($this->validateStage)(new \stdClass(), $resourceClass, $operation, []); - } - - public function testApply(): void - { - $resourceClass = 'myResource'; - $validationGroups = ['group']; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query')->withValidationContext(['groups' => $validationGroups]); - - $object = new \stdClass(); - $this->validatorProphecy->validate($object, ['groups' => $validationGroups])->shouldBeCalled(); - - ($this->validateStage)($object, $resourceClass, $operation, []); - } - - public function testApplyNotValidated(): void - { - $resourceClass = 'myResource'; - $validationGroups = ['group']; - /** @var Operation $operation */ - $operation = (new Query())->withValidationContext(['groups' => $validationGroups])->withName('item_query'); - $info = $this->prophesize(ResolveInfo::class)->reveal(); - $context = ['info' => $info]; - - $object = new \stdClass(); - $this->validatorProphecy->validate($object, ['groups' => $validationGroups])->shouldBeCalled()->willThrow(new ValidationException(new ConstraintViolationList())); - - $this->expectException(ValidationException::class); - - ($this->validateStage)($object, $resourceClass, $operation, $context); - } -} diff --git a/src/GraphQl/Tests/Resolver/Stage/WriteStageTest.php b/src/GraphQl/Tests/Resolver/Stage/WriteStageTest.php deleted file mode 100644 index 25544062faf..00000000000 --- a/src/GraphQl/Tests/Resolver/Stage/WriteStageTest.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Tests\Resolver\Stage; - -use ApiPlatform\GraphQl\Resolver\Stage\WriteStage; -use ApiPlatform\GraphQl\Serializer\SerializerContextBuilderInterface; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\State\ProcessorInterface; -use PHPUnit\Framework\TestCase; -use Prophecy\PhpUnit\ProphecyTrait; -use Prophecy\Prophecy\ObjectProphecy; - -/** - * @author Alan Poulain - */ -class WriteStageTest extends TestCase -{ - use ProphecyTrait; - - private WriteStage $writeStage; - private ObjectProphecy $processorProphecy; - private ObjectProphecy $serializerContextBuilderProphecy; - - /** - * {@inheritdoc} - */ - protected function setUp(): void - { - $this->processorProphecy = $this->prophesize(ProcessorInterface::class); - $this->serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class); - - $this->writeStage = new WriteStage( - $this->processorProphecy->reveal(), - $this->serializerContextBuilderProphecy->reveal() - ); - } - - public function testNoData(): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query'); - - $result = ($this->writeStage)(null, $resourceClass, $operation, []); - - $this->assertNull($result); - } - - public function testApplyDisabled(): void - { - $resourceClass = 'myResource'; - /** @var Operation $operation */ - $operation = (new Query())->withName('item_query')->withWrite(false); - - $data = new \stdClass(); - $result = ($this->writeStage)($data, $resourceClass, $operation, []); - - $this->assertSame($data, $result); - } - - public function testApply(): void - { - $operationName = 'create'; - $resourceClass = 'myResource'; - $context = []; - /** @var Operation $operation */ - $operation = (new Mutation())->withName($operationName); - - $denormalizationContext = ['denormalization' => true]; - $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, false)->willReturn($denormalizationContext); - - $data = new \stdClass(); - $processedData = new \stdClass(); - $this->processorProphecy->process($data, $operation, [], ['operation' => $operation] + $denormalizationContext)->shouldBeCalled()->willReturn($processedData); - - $result = ($this->writeStage)($data, $resourceClass, $operation, $context); - - $this->assertSame($processedData, $result); - } -} diff --git a/src/GraphQl/Tests/Serializer/Exception/HttpExceptionNormalizerTest.php b/src/GraphQl/Tests/Serializer/Exception/HttpExceptionNormalizerTest.php index 9a66078a65e..2c54b9c4235 100644 --- a/src/GraphQl/Tests/Serializer/Exception/HttpExceptionNormalizerTest.php +++ b/src/GraphQl/Tests/Serializer/Exception/HttpExceptionNormalizerTest.php @@ -35,9 +35,7 @@ protected function setUp(): void $this->httpExceptionNormalizer = new HttpExceptionNormalizer(); } - /** - * @dataProvider exceptionProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('exceptionProvider')] public function testNormalize(HttpException $exception, string $expectedExceptionMessage, int $expectedStatus, string $expectedCategory): void { $error = new Error('test message', null, null, [], null, $exception); diff --git a/src/GraphQl/Tests/Serializer/ItemNormalizerTest.php b/src/GraphQl/Tests/Serializer/ItemNormalizerTest.php index 559d13b1e58..de8a4b2e8b8 100644 --- a/src/GraphQl/Tests/Serializer/ItemNormalizerTest.php +++ b/src/GraphQl/Tests/Serializer/ItemNormalizerTest.php @@ -78,11 +78,11 @@ public function testNormalize(): void $propertyNameCollection = new PropertyNameCollection(['name']); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn($propertyNameCollection); $propertyMetadata = (new ApiProperty())->withReadable(true); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadata); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn($propertyMetadata); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($dummy, UrlGeneratorInterface::ABS_URL, Argument::any(), Argument::type('array'))->willReturn('/dummies/1'); @@ -131,13 +131,13 @@ public function testNormalizeWithUnsafeCacheProperty(): void $propertyNameCollection = new PropertyNameCollection(['title', 'ownerOnlyProperty']); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn($propertyNameCollection); + $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, Argument::type('array'))->willReturn($propertyNameCollection); $unsecuredPropertyMetadata = (new ApiProperty())->withReadable(true); $securedPropertyMetadata = (new ApiProperty())->withReadable(true)->withSecurity('object == null or object.getOwner() == user'); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn($unsecuredPropertyMetadata); - $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn($securedPropertyMetadata); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', Argument::type('array'))->willReturn($unsecuredPropertyMetadata); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', Argument::type('array'))->willReturn($securedPropertyMetadata); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($securedDummyWithOwnerOnlyPropertyAllowed, UrlGeneratorInterface::ABS_URL, Argument::any(), Argument::type('array'))->willReturn('/dummies/1'); @@ -216,11 +216,11 @@ public function testNormalizeNoResolverData(): void $propertyNameCollection = new PropertyNameCollection(['name']); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn($propertyNameCollection); $propertyMetadata = (new ApiProperty())->withWritable(true)->withReadable(true); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadata); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn($propertyMetadata); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource($dummy, UrlGeneratorInterface::ABS_URL, Argument::any(), Argument::type('array'))->willReturn('/dummies/1'); @@ -260,11 +260,11 @@ public function testDenormalize(): void $propertyNameCollection = new PropertyNameCollection(['name']); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection)->shouldBeCalled(); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn($propertyNameCollection)->shouldBeCalled(); $propertyMetadata = (new ApiProperty())->withWritable(true)->withReadable(true); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn($propertyMetadata)->shouldBeCalled(); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); diff --git a/src/GraphQl/Tests/Serializer/SerializerContextBuilderTest.php b/src/GraphQl/Tests/Serializer/SerializerContextBuilderTest.php index 847169ef871..d4bc15990dc 100644 --- a/src/GraphQl/Tests/Serializer/SerializerContextBuilderTest.php +++ b/src/GraphQl/Tests/Serializer/SerializerContextBuilderTest.php @@ -77,14 +77,10 @@ private function buildOperationFromContext(bool $isMutation, bool $isSubscriptio } } - \assert($operation instanceof Operation); - return $operation; } - /** - * @dataProvider createNormalizationContextProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createNormalizationContextProvider')] public function testCreateNormalizationContext(?string $resourceClass, string $operationName, array $fields, bool $isMutation, bool $isSubscription, bool $noInfo, array $expectedContext, ?callable $advancedNameConverter = null, ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void { $resolverContext = []; @@ -296,9 +292,7 @@ public static function createNormalizationContextProvider(): iterable ]; } - /** - * @dataProvider createDenormalizationContextProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('createDenormalizationContextProvider')] public function testCreateDenormalizationContext(?string $resourceClass, string $operationName, array $expectedContext): void { $operation = $this->buildOperationFromContext(true, false, $expectedContext, false, $resourceClass); diff --git a/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php b/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php index e34c8a3b743..43baca2ce1f 100644 --- a/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php +++ b/src/GraphQl/Tests/State/Processor/NormalizeProcessorTest.php @@ -42,16 +42,14 @@ protected function setUp(): void $this->resolveInfoProphecy = $this->prophesize(ResolveInfo::class); } - /** - * @dataProvider processItems - */ + #[\PHPUnit\Framework\Attributes\DataProvider('processItems')] public function testProcess($body, $operation): void { $context = ['args' => []]; $serializerContext = ['resource_class' => $operation->getClass()]; $normalizer = $this->createMock(NormalizerInterface::class); $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); - $serializerContextBuilder->expects($this->once())->method('create')->with($operation->getClass(), $operation, $context, normalization: true)->willReturn($serializerContext); + $serializerContextBuilder->expects($this->once())->method('create')->with($operation->getClass(), $operation, $context, true)->willReturn($serializerContext); $normalizer->expects($this->once())->method('normalize')->with($body, 'graphql', $serializerContext); $processor = new NormalizeProcessor($normalizer, $serializerContextBuilder, new Pagination()); $processor->process($body, $operation, [], $context); @@ -60,15 +58,13 @@ public function testProcess($body, $operation): void public static function processItems(): array { return [ - [new \stdClass(), new Query(class: 'foo')], - [new \stdClass(), new Mutation(class: 'foo', shortName: 'Foo')], - [new \stdClass(), new Subscription(class: 'foo', shortName: 'Foo')], + [new \stdClass(), new Query(class: \stdClass::class)], + [new \stdClass(), new Mutation(class: \stdClass::class, shortName: 'Foo')], + [new \stdClass(), new Subscription(class: \stdClass::class, shortName: 'Foo')], ]; } - /** - * @dataProvider processCollection - */ + #[\PHPUnit\Framework\Attributes\DataProvider('processCollection')] public function testProcessCollection($collection, $operation, $args, ?array $expectedResult, array $getFieldSelection, ?string $expectedExceptionClass = null, ?string $expectedExceptionMessage = null): void { $this->resolveInfoProphecy->getFieldSelection(1)->willReturn($getFieldSelection); @@ -77,7 +73,7 @@ public function testProcessCollection($collection, $operation, $args, ?array $ex $normalizer = $this->prophesize(NormalizerInterface::class); $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); - $serializerContextBuilder->expects($this->once())->method('create')->with($operation->getClass(), $operation, $context, normalization: true)->willReturn($serializerContext); + $serializerContextBuilder->expects($this->once())->method('create')->with($operation->getClass(), $operation, $context, true)->willReturn($serializerContext); foreach ($collection as $v) { $normalizer->normalize($v, 'graphql', $serializerContext)->willReturn(['normalized_item'])->shouldBeCalledOnce(); } @@ -104,34 +100,34 @@ public static function processCollection(): iterable return $partialPaginatorProphecy->reveal(); }; - yield 'cursor - not paginator' => [[], new QueryCollection(class: 'foo'), [], null, [], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface or ApiPlatform\State\Pagination\PartialPaginatorInterface.']; - yield 'cursor - empty paginator' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: 'foo'), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), new QueryCollection(class: 'foo'), [], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MA=='], ['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => false]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with after cursor' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 1, 2), new QueryCollection(class: 'foo'), ['after' => 'MA=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with bad after cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: 'foo'), ['after' => '-'], null, ['edges' => ['cursor' => []]], \UnexpectedValueException::class, 'Cursor - is invalid']; - yield 'cursor - paginator with empty after cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: 'foo'), ['after' => ''], null, ['edges' => ['cursor' => []]], \UnexpectedValueException::class, 'Empty cursor is invalid']; - yield 'cursor - paginator with before cursor' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 1, 1), new QueryCollection(class: 'foo'), ['before' => 'Mg=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - paginator with bad before cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: 'foo'), ['before' => '-'], null, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; - yield 'cursor - paginator with empty before cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: 'foo'), ['before' => ''], null, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; - yield 'cursor - paginator with last' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 1, 2), new QueryCollection(class: 'foo'), ['last' => 2], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - partial paginator' => [$partialPaginatorFactory, new QueryCollection(class: 'foo'), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => false, 'hasPreviousPage' => false]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - yield 'cursor - partial paginator with after cursor' => [$partialPaginatorFactory, new QueryCollection(class: 'foo'), ['after' => 'MA=='], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; - - yield 'page - not paginator, itemsPerPage requested' => [[], (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], [], ['paginationInfo' => ['itemsPerPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PartialPaginatorInterface to return itemsPerPage field.']; - yield 'page - not paginator, lastPage requested' => [[], (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], [], ['paginationInfo' => ['lastPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return lastPage field.']; - yield 'page - not paginator, totalCount requested' => [[], (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], [], ['paginationInfo' => ['totalCount' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return totalCount field.']; - yield 'page - not paginator, hasNextPage requested' => [[], (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], [], ['paginationInfo' => ['hasNextPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\HasNextPagePaginatorInterface to return hasNextPage field.']; - yield 'page - empty paginator - itemsPerPage requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['itemsPerPage' => .0]], ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - empty paginator - lastPage requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['lastPage' => 1.0]], ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - empty paginator - totalCount requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['totalCount' => .0]], ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - empty paginator - hasNextPage requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['hasNextPage' => false]], ['paginationInfo' => ['hasNextPage' => true]]]; - yield 'page - paginator page 1 - itemsPerPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - paginator page 1 - lastPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - paginator page 1 - totalCount requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - paginator page 1 - hasNextPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['hasNextPage' => true]], ['paginationInfo' => ['hasNextPage' => true]]]; - yield 'page - paginator with page - itemsPerPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], ['paginationInfo' => ['itemsPerPage' => true]]]; - yield 'page - paginator with page - lastPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], ['paginationInfo' => ['lastPage' => true]]]; - yield 'page - paginator with page - totalCount requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], ['paginationInfo' => ['totalCount' => true]]]; - yield 'page - paginator with page - hasNextPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: 'foo'))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['hasNextPage' => false]], ['paginationInfo' => ['hasNextPage' => true]]]; + yield 'cursor - not paginator' => [[], new QueryCollection(class: \stdClass::class), [], null, [], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface or ApiPlatform\State\Pagination\PartialPaginatorInterface.']; + yield 'cursor - empty paginator' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: \stdClass::class), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => null, 'endCursor' => null, 'hasNextPage' => false, 'hasPreviousPage' => false]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), new QueryCollection(class: \stdClass::class), [], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MA=='], ['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => false]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator with after cursor' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 1, 2), new QueryCollection(class: \stdClass::class), ['after' => 'MA=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator with bad after cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: \stdClass::class), ['after' => '-'], null, ['edges' => ['cursor' => []]], \UnexpectedValueException::class, 'Cursor - is invalid']; + yield 'cursor - paginator with empty after cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: \stdClass::class), ['after' => ''], null, ['edges' => ['cursor' => []]], \UnexpectedValueException::class, 'Empty cursor is invalid']; + yield 'cursor - paginator with before cursor' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 1, 1), new QueryCollection(class: \stdClass::class), ['before' => 'Mg=='], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'MQ==', 'hasNextPage' => true, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - paginator with bad before cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: \stdClass::class), ['before' => '-'], null, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Cursor - is invalid']; + yield 'cursor - paginator with empty before cursor' => [new ArrayPaginator([], 0, 0), new QueryCollection(class: \stdClass::class), ['before' => ''], null, ['pageInfo' => ['endCursor' => true]], \UnexpectedValueException::class, 'Empty cursor is invalid']; + yield 'cursor - paginator with last' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 1, 2), new QueryCollection(class: \stdClass::class), ['last' => 2], ['totalCount' => 3., 'edges' => [['node' => ['normalized_item'], 'cursor' => 'MQ=='], ['node' => ['normalized_item'], 'cursor' => 'Mg==']], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - partial paginator' => [$partialPaginatorFactory, new QueryCollection(class: \stdClass::class), [], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MA==', 'endCursor' => 'MQ==', 'hasNextPage' => false, 'hasPreviousPage' => false]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + yield 'cursor - partial paginator with after cursor' => [$partialPaginatorFactory, new QueryCollection(class: \stdClass::class), ['after' => 'MA=='], ['totalCount' => 0., 'edges' => [], 'pageInfo' => ['startCursor' => 'MQ==', 'endCursor' => 'Mg==', 'hasNextPage' => false, 'hasPreviousPage' => true]], ['totalCount' => true, 'edges' => ['cursor' => true], 'pageInfo' => ['startCursor' => true, 'endCursor' => true, 'hasNextPage' => true, 'hasPreviousPage' => true]]]; + + yield 'page - not paginator, itemsPerPage requested' => [[], (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], [], ['paginationInfo' => ['itemsPerPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PartialPaginatorInterface to return itemsPerPage field.']; + yield 'page - not paginator, lastPage requested' => [[], (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], [], ['paginationInfo' => ['lastPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return lastPage field.']; + yield 'page - not paginator, totalCount requested' => [[], (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], [], ['paginationInfo' => ['totalCount' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\PaginatorInterface to return totalCount field.']; + yield 'page - not paginator, hasNextPage requested' => [[], (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], [], ['paginationInfo' => ['hasNextPage' => true]], \LogicException::class, 'Collection returned by the collection data provider must implement ApiPlatform\State\Pagination\HasNextPagePaginatorInterface to return hasNextPage field.']; + yield 'page - empty paginator - itemsPerPage requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['itemsPerPage' => .0]], ['paginationInfo' => ['itemsPerPage' => true]]]; + yield 'page - empty paginator - lastPage requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['lastPage' => 1.0]], ['paginationInfo' => ['lastPage' => true]]]; + yield 'page - empty paginator - totalCount requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['totalCount' => .0]], ['paginationInfo' => ['totalCount' => true]]]; + yield 'page - empty paginator - hasNextPage requested' => [new ArrayPaginator([], 0, 0), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [], 'paginationInfo' => ['hasNextPage' => false]], ['paginationInfo' => ['hasNextPage' => true]]]; + yield 'page - paginator page 1 - itemsPerPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], ['paginationInfo' => ['itemsPerPage' => true]]]; + yield 'page - paginator page 1 - lastPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], ['paginationInfo' => ['lastPage' => true]]]; + yield 'page - paginator page 1 - totalCount requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], ['paginationInfo' => ['totalCount' => true]]]; + yield 'page - paginator page 1 - hasNextPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 0, 2), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [['normalized_item'], ['normalized_item']], 'paginationInfo' => ['hasNextPage' => true]], ['paginationInfo' => ['hasNextPage' => true]]]; + yield 'page - paginator with page - itemsPerPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['itemsPerPage' => 2.0]], ['paginationInfo' => ['itemsPerPage' => true]]]; + yield 'page - paginator with page - lastPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['lastPage' => 2.0]], ['paginationInfo' => ['lastPage' => true]]]; + yield 'page - paginator with page - totalCount requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['totalCount' => 3.0]], ['paginationInfo' => ['totalCount' => true]]]; + yield 'page - paginator with page - hasNextPage requested' => [new ArrayPaginator([(object) ['test' => 'a'], (object) ['test' => 'b'], (object) ['test' => 'c']], 2, 2), (new QueryCollection(class: \stdClass::class))->withPaginationType('page'), [], ['collection' => [['normalized_item']], 'paginationInfo' => ['hasNextPage' => false]], ['paginationInfo' => ['hasNextPage' => true]]]; } } diff --git a/src/GraphQl/Tests/State/Provider/DenormalizeProviderTest.php b/src/GraphQl/Tests/State/Provider/DenormalizeProviderTest.php index d356b1f92e6..b1232566245 100644 --- a/src/GraphQl/Tests/State/Provider/DenormalizeProviderTest.php +++ b/src/GraphQl/Tests/State/Provider/DenormalizeProviderTest.php @@ -27,14 +27,14 @@ public function testProvide(): void { $objectToPopulate = null; $context = ['args' => ['input' => ['test']]]; - $operation = new Mutation(class: 'dummy'); + $operation = new Mutation(class: \stdClass::class); $serializerContext = ['resource_class' => $operation->getClass()]; $decorated = $this->createMock(ProviderInterface::class); $decorated->expects($this->once())->method('provide')->willReturn($objectToPopulate); $denormalizer = $this->createMock(DenormalizerInterface::class); $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); - $serializerContextBuilder->expects($this->once())->method('create')->with($operation->getClass(), $operation, $context, normalization: false)->willReturn($serializerContext); - $denormalizer->expects($this->once())->method('denormalize')->with(['test'], 'dummy', 'graphql', $serializerContext)->willReturn(new \stdClass()); + $serializerContextBuilder->expects($this->once())->method('create')->with($operation->getClass(), $operation, $context, false)->willReturn($serializerContext); + $denormalizer->expects($this->once())->method('denormalize')->with(['test'], \stdClass::class, 'graphql', $serializerContext)->willReturn(new \stdClass()); $provider = new DenormalizeProvider($decorated, $denormalizer, $serializerContextBuilder); $provider->provide($operation, [], $context); } @@ -43,14 +43,14 @@ public function testProvideWithObjectToPopulate(): void { $objectToPopulate = new \stdClass(); $context = ['args' => ['input' => ['test']]]; - $operation = new Mutation(class: 'dummy'); + $operation = new Mutation(class: \stdClass::class); $serializerContext = ['resource_class' => $operation->getClass(), 'object_to_populate' => $objectToPopulate]; $decorated = $this->createMock(ProviderInterface::class); $decorated->expects($this->once())->method('provide')->willReturn($objectToPopulate); $denormalizer = $this->createMock(DenormalizerInterface::class); $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); - $serializerContextBuilder->expects($this->once())->method('create')->with($operation->getClass(), $operation, $context, normalization: false)->willReturn($serializerContext); - $denormalizer->expects($this->once())->method('denormalize')->with(['test'], 'dummy', 'graphql', $serializerContext)->willReturn(new \stdClass()); + $serializerContextBuilder->expects($this->once())->method('create')->with($operation->getClass(), $operation, $context, false)->willReturn($serializerContext); + $denormalizer->expects($this->once())->method('denormalize')->with(['test'], \stdClass::class, 'graphql', $serializerContext)->willReturn(new \stdClass()); $provider = new DenormalizeProvider($decorated, $denormalizer, $serializerContextBuilder); $provider->provide($operation, [], $context); } @@ -59,14 +59,14 @@ public function testProvideNotCalledWithQuery(): void { $objectToPopulate = new \stdClass(); $context = ['args' => ['input' => ['test']]]; - $operation = new Query(class: 'dummy'); + $operation = new Query(class: \stdClass::class); $serializerContext = ['resource_class' => $operation->getClass()]; $decorated = $this->createMock(ProviderInterface::class); $decorated->expects($this->once())->method('provide')->willReturn($objectToPopulate); $denormalizer = $this->createMock(DenormalizerInterface::class); $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); - $serializerContextBuilder->expects($this->never())->method('create')->with($operation->getClass(), $operation, $context, normalization: false)->willReturn($serializerContext); - $denormalizer->expects($this->never())->method('denormalize')->with(['test'], 'dummy', 'graphql', $serializerContext)->willReturn(new \stdClass()); + $serializerContextBuilder->expects($this->never())->method('create')->with($operation->getClass(), $operation, $context, false)->willReturn($serializerContext); + $denormalizer->expects($this->never())->method('denormalize')->with(['test'], \stdClass::class, 'graphql', $serializerContext)->willReturn(new \stdClass()); $provider = new DenormalizeProvider($decorated, $denormalizer, $serializerContextBuilder); $provider->provide($operation, [], $context); } @@ -75,14 +75,14 @@ public function testProvideNotCalledWithoutDeserialize(): void { $objectToPopulate = new \stdClass(); $context = ['args' => ['input' => ['test']]]; - $operation = new Query(class: 'dummy', deserialize: false); + $operation = new Query(class: \stdClass::class, deserialize: false); $serializerContext = ['resource_class' => $operation->getClass()]; $decorated = $this->createMock(ProviderInterface::class); $decorated->expects($this->once())->method('provide')->willReturn($objectToPopulate); $denormalizer = $this->createMock(DenormalizerInterface::class); $serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class); - $serializerContextBuilder->expects($this->never())->method('create')->with($operation->getClass(), $operation, $context, normalization: false)->willReturn($serializerContext); - $denormalizer->expects($this->never())->method('denormalize')->with(['test'], 'dummy', 'graphql', $serializerContext)->willReturn(new \stdClass()); + $serializerContextBuilder->expects($this->never())->method('create')->with($operation->getClass(), $operation, $context, false)->willReturn($serializerContext); + $denormalizer->expects($this->never())->method('denormalize')->with(['test'], \stdClass::class, 'graphql', $serializerContext)->willReturn(new \stdClass()); $provider = new DenormalizeProvider($decorated, $denormalizer, $serializerContextBuilder); $provider->provide($operation, [], $context); } diff --git a/src/GraphQl/Tests/State/Provider/ReadProviderTest.php b/src/GraphQl/Tests/State/Provider/ReadProviderTest.php index 322cc41bfe4..871cb144f7b 100644 --- a/src/GraphQl/Tests/State/Provider/ReadProviderTest.php +++ b/src/GraphQl/Tests/State/Provider/ReadProviderTest.php @@ -28,7 +28,7 @@ class ReadProviderTest extends TestCase public function testProvide(): void { $context = ['args' => ['id' => '/dummy/1']]; - $operation = new Query(class: 'dummy'); + $operation = new Query(class: \stdClass::class); $decorated = $this->createMock(ProviderInterface::class); $iriConverter = $this->createMock(IriConverterInterface::class); $iriConverter->expects($this->once())->method('getResourceFromIri')->with('/dummy/1'); @@ -45,7 +45,7 @@ public function testProvide(): void public function testProvideNotExistedResource(): void { $context = ['args' => ['id' => '/dummy/1']]; - $operation = new Query(class: 'dummy'); + $operation = new Query(class: \stdClass::class); $decorated = $this->createMock(ProviderInterface::class); $iriConverter = $this->createMock(IriConverterInterface::class); $iriConverter->expects($this->once())->method('getResourceFromIri')->with('/dummy/1'); @@ -61,8 +61,8 @@ public function testProvideCollection(): void { $info = $this->createMock(ResolveInfo::class); $info->fieldName = ''; - $context = ['root_class' => 'dummy', 'source' => [], 'info' => $info, 'filters' => []]; - $operation = new QueryCollection(class: 'dummy'); + $context = ['root_class' => \stdClass::class, 'source' => [], 'info' => $info, 'filters' => []]; + $operation = new QueryCollection(class: \stdClass::class); $decorated = $this->createMock(ProviderInterface::class); $decorated->expects($this->once())->method('provide')->with($operation, [], ['a'] + $context); $iriConverter = $this->createMock(IriConverterInterface::class); diff --git a/src/GraphQl/Tests/State/Provider/ResolverProviderTest.php b/src/GraphQl/Tests/State/Provider/ResolverProviderTest.php index 64d9e9b8220..08f32d4f75a 100644 --- a/src/GraphQl/Tests/State/Provider/ResolverProviderTest.php +++ b/src/GraphQl/Tests/State/Provider/ResolverProviderTest.php @@ -28,7 +28,7 @@ class ResolverProviderTest extends TestCase public function testProvide(): void { $res = new \stdClass(); - $operation = new QueryCollection(class: 'dummy', resolver: 'foo'); + $operation = new QueryCollection(class: \stdClass::class, resolver: 'foo'); $context = []; $decorated = $this->createMock(ProviderInterface::class); $resolverMock = $this->createMock(QueryItemResolverInterface::class); diff --git a/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php b/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php index b8fcbd22e3f..7afeaeaef03 100644 --- a/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php +++ b/src/GraphQl/Tests/Subscription/SubscriptionManagerTest.php @@ -13,7 +13,6 @@ namespace ApiPlatform\GraphQl\Tests\Subscription; -use ApiPlatform\GraphQl\Resolver\Stage\SerializeStageInterface; use ApiPlatform\GraphQl\Subscription\SubscriptionIdentifierGeneratorInterface; use ApiPlatform\GraphQl\Subscription\SubscriptionManager; use ApiPlatform\GraphQl\Tests\Fixtures\ApiResource\Dummy; @@ -24,6 +23,7 @@ use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\State\ProcessorInterface; use GraphQL\Type\Definition\ResolveInfo; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; @@ -40,7 +40,7 @@ class SubscriptionManagerTest extends TestCase private ObjectProphecy $subscriptionsCacheProphecy; private ObjectProphecy $subscriptionIdentifierGeneratorProphecy; - private ObjectProphecy $serializeStageProphecy; + private ObjectProphecy $normalizeProcessor; private ObjectProphecy $iriConverterProphecy; private SubscriptionManager $subscriptionManager; private ObjectProphecy $resourceMetadataCollectionFactory; @@ -52,10 +52,10 @@ protected function setUp(): void { $this->subscriptionsCacheProphecy = $this->prophesize(CacheItemPoolInterface::class); $this->subscriptionIdentifierGeneratorProphecy = $this->prophesize(SubscriptionIdentifierGeneratorInterface::class); - $this->serializeStageProphecy = $this->prophesize(SerializeStageInterface::class); + $this->normalizeProcessor = $this->prophesize(ProcessorInterface::class); $this->iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $this->resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); - $this->subscriptionManager = new SubscriptionManager($this->subscriptionsCacheProphecy->reveal(), $this->subscriptionIdentifierGeneratorProphecy->reveal(), $this->serializeStageProphecy->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceMetadataCollectionFactory->reveal()); + $this->subscriptionManager = new SubscriptionManager($this->subscriptionsCacheProphecy->reveal(), $this->subscriptionIdentifierGeneratorProphecy->reveal(), $this->normalizeProcessor->reveal(), $this->iriConverterProphecy->reveal(), $this->resourceMetadataCollectionFactory->reveal()); } public function testRetrieveSubscriptionIdNoIdentifier(): void @@ -206,8 +206,23 @@ public function testGetPushPayloadsHit(): void ]); $this->subscriptionsCacheProphecy->getItem('_dummies_2')->willReturn($cacheItemProphecy->reveal()); - $this->serializeStageProphecy->__invoke($object, Dummy::class, (new Subscription())->withName('update_subscription')->withShortName('Dummy'), ['fields' => ['fieldsFoo'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true])->willReturn(['newResultFoo', 'clientSubscriptionId' => 'client-subscription-id']); - $this->serializeStageProphecy->__invoke($object, Dummy::class, (new Subscription())->withName('update_subscription')->withShortName('Dummy'), ['fields' => ['fieldsBar'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true])->willReturn(['resultBar', 'clientSubscriptionId' => 'client-subscription-id']); + $this->normalizeProcessor->process( + $object, + (new Subscription())->withName('update_subscription')->withShortName('Dummy'), + [], + ['fields' => ['fieldsFoo'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true] + )->willReturn( + ['newResultFoo', 'clientSubscriptionId' => 'client-subscription-id'] + ); + + $this->normalizeProcessor->process( + $object, + (new Subscription())->withName('update_subscription')->withShortName('Dummy'), + [], + ['fields' => ['fieldsBar'], 'is_collection' => false, 'is_mutation' => false, 'is_subscription' => true] + )->willReturn( + ['resultBar', 'clientSubscriptionId' => 'client-subscription-id'] + ); $this->assertEquals([['subscriptionIdFoo', ['newResultFoo']]], $this->subscriptionManager->getPushPayloads($object)); } diff --git a/src/GraphQl/Tests/Type/FieldsBuilderTest.php b/src/GraphQl/Tests/Type/FieldsBuilderTest.php index 8b24844a0ca..dc95924e9af 100644 --- a/src/GraphQl/Tests/Type/FieldsBuilderTest.php +++ b/src/GraphQl/Tests/Type/FieldsBuilderTest.php @@ -46,8 +46,8 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Container\ContainerInterface; -use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; +use Symfony\Component\TypeInfo\Type; /** * @author Alan Poulain @@ -63,9 +63,6 @@ class FieldsBuilderTest extends TestCase private ObjectProphecy $typeBuilderProphecy; private ObjectProphecy $typeConverterProphecy; private ObjectProphecy $itemResolverFactoryProphecy; - private ObjectProphecy $collectionResolverFactoryProphecy; - private ObjectProphecy $itemMutationResolverFactoryProphecy; - private ObjectProphecy $itemSubscriptionResolverFactoryProphecy; private ObjectProphecy $filterLocatorProphecy; private ObjectProphecy $resourceClassResolverProphecy; private FieldsBuilder $fieldsBuilder; @@ -82,9 +79,6 @@ protected function setUp(): void $this->typeBuilderProphecy = $this->prophesize(ContextAwareTypeBuilderInterface::class); $this->typeConverterProphecy = $this->prophesize(TypeConverterInterface::class); $this->itemResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); - $this->collectionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); - $this->itemMutationResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); - $this->itemSubscriptionResolverFactoryProphecy = $this->prophesize(ResolverFactoryInterface::class); $this->filterLocatorProphecy = $this->prophesize(ContainerInterface::class); $this->resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $this->fieldsBuilder = $this->buildFieldsBuilder(); @@ -92,7 +86,7 @@ protected function setUp(): void private function buildFieldsBuilder(?AdvancedNameConverterInterface $advancedNameConverter = null): FieldsBuilder { - return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->collectionResolverFactoryProphecy->reveal(), $this->itemMutationResolverFactoryProphecy->reveal(), $this->itemSubscriptionResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); + return new FieldsBuilder($this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->typeBuilderProphecy->reveal(), $this->typeConverterProphecy->reveal(), $this->itemResolverFactoryProphecy->reveal(), $this->filterLocatorProphecy->reveal(), new Pagination(), $advancedNameConverter ?? new CustomConverter(), '__'); } public function testGetNodeQueryFields(): void @@ -119,16 +113,13 @@ public function testGetNodeQueryFields(): void $this->assertSame($itemResolver, $nodeQueryFields['resolve']); } - /** - * @dataProvider itemQueryFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('itemQueryFieldsProvider')] public function testGetItemQueryFields(string $resourceClass, Operation $operation, array $configuration, ?GraphQLType $graphqlType, ?callable $resolver, array $expectedQueryFields): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); + $this->typeConverterProphecy->convertPhpType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); - $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); + $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation, Argument::any())->willReturn($resolver); $queryFields = $this->fieldsBuilder->getItemQueryFields($resourceClass, $operation, $configuration); @@ -138,10 +129,10 @@ public function testGetItemQueryFields(string $resourceClass, Operation $operati public static function itemQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action'), [], null, null, []], - 'nested item query' => ['resourceClass', (new Query())->withNested(true)->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], new ObjectType(['name' => 'item', 'fields' => []]), function (): void { + 'no resource field configuration' => [\stdClass::class, (new Query())->withClass(\stdClass::class)->withName('action'), [], null, null, []], + 'nested item query' => [\stdClass::class, (new Query())->withNested(true)->withClass(\stdClass::class)->withName('action')->withShortName('ShortName'), [], new ObjectType(['name' => 'item', 'fields' => []]), function (): void { }, []], - 'nominal standard type case with deprecation reason and description' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, + 'nominal standard type case with deprecation reason and description' => [\stdClass::class, (new Query())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], GraphQLType::string(), null, [ 'actionShortName' => [ 'type' => GraphQLType::string(), @@ -154,7 +145,7 @@ public static function itemQueryFieldsProvider(): array ], ], ], - 'nominal item case' => ['resourceClass', (new Query())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item', 'fields' => []]), $resolver = function (): void { + 'nominal item case' => [\stdClass::class, (new Query())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName'), [], $graphqlType = new ObjectType(['name' => 'item', 'fields' => []]), $resolver = function (): void { }, [ 'actionShortName' => [ @@ -169,7 +160,7 @@ public static function itemQueryFieldsProvider(): array ], ], 'empty overridden args and add fields' => [ - 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, + \stdClass::class, (new Query())->withClass(\stdClass::class)->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -182,7 +173,7 @@ public static function itemQueryFieldsProvider(): array ], ], 'override args with custom ones' => [ - 'resourceClass', (new Query())->withClass('resourceClass')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, + \stdClass::class, (new Query())->withClass(\stdClass::class)->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], GraphQLType::string(), null, [ 'shortName' => [ 'type' => GraphQLType::string(), @@ -200,23 +191,20 @@ public static function itemQueryFieldsProvider(): array ]; } - /** - * @dataProvider collectionQueryFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('collectionQueryFieldsProvider')] public function testGetCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration, ?GraphQLType $graphqlType, ?callable $resolver, array $expectedQueryFields): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); + $this->typeConverterProphecy->convertPhpType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); - $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(true); $this->typeBuilderProphecy->getPaginatedCollectionType($graphqlType, $operation)->willReturn($graphqlType); - $this->collectionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($resolver); + $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation, Argument::any())->willReturn($resolver); $this->filterLocatorProphecy->has('my_filter')->willReturn(true); $filterProphecy = $this->prophesize(FilterInterface::class); $filterProphecy->getDescription($resourceClass)->willReturn([ 'boolField' => ['type' => 'bool', 'required' => true], - 'boolField[]' => ['type' => 'bool', 'required' => true], - 'parent.child[related.nested]' => ['type' => 'bool', 'required' => true], + 'boolField[]' => ['type' => 'bool', 'required' => false], + 'parent.child[related.nested]' => ['type' => 'bool', 'required' => false], 'dateField[before]' => ['type' => \DateTimeInterface::class, 'required' => false], ]); $this->filterLocatorProphecy->get('my_filter')->willReturn($filterProphecy->reveal()); @@ -233,10 +221,10 @@ public function testGetCollectionQueryFields(string $resourceClass, Operation $o public static function collectionQueryFieldsProvider(): array { return [ - 'no resource field configuration' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action'), [], null, null, []], - 'nested collection query' => ['resourceClass', (new QueryCollection())->withNested(true)->withClass('resourceClass')->withName('action')->withShortName('ShortName'), [], GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), function (): void { + 'no resource field configuration' => [\stdClass::class, (new QueryCollection())->withClass(\stdClass::class)->withName('action'), [], null, null, []], + 'nested collection query' => [\stdClass::class, (new QueryCollection())->withNested(true)->withClass(\stdClass::class)->withName('action')->withShortName('ShortName'), [], GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), function (): void { }, []], - 'nominal collection case with deprecation reason and description' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { + 'nominal collection case with deprecation reason and description' => [\stdClass::class, (new QueryCollection())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful')->withDescription('Custom description.'), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -265,7 +253,7 @@ public static function collectionQueryFieldsProvider(): array ], ], ], - 'collection with filters' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { + 'collection with filters' => [\stdClass::class, (new QueryCollection())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -288,7 +276,7 @@ public static function collectionQueryFieldsProvider(): array 'type' => GraphQLType::string(), 'description' => 'Returns the elements in the list that come after the specified cursor.', ], - 'boolField' => $graphqlType, + 'boolField' => GraphQLType::nonNull($graphqlType), 'boolField_list' => GraphQLType::listOf($graphqlType), 'parent__child' => GraphQLType::listOf(new InputObjectType(['name' => 'ShortNameFilter_parent__child', 'fields' => ['related__nested' => $graphqlType]])), 'dateField' => GraphQLType::listOf(new InputObjectType(['name' => 'ShortNameFilter_dateField', 'fields' => ['before' => $graphqlType]])), @@ -299,7 +287,7 @@ public static function collectionQueryFieldsProvider(): array ], ], 'collection empty overridden args and add fields' => [ - 'resourceClass', (new QueryCollection())->withArgs([])->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { + \stdClass::class, (new QueryCollection())->withArgs([])->withClass(\stdClass::class)->withName('action')->withShortName('ShortName'), ['args' => [], 'name' => 'customActionName'], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -313,7 +301,7 @@ public static function collectionQueryFieldsProvider(): array ], ], 'collection override args with custom ones' => [ - 'resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { + \stdClass::class, (new QueryCollection())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName'), ['args' => ['customArg' => ['type' => 'a type']]], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -329,7 +317,7 @@ public static function collectionQueryFieldsProvider(): array ], ], ], - 'collection with page-based pagination enabled' => ['resourceClass', (new QueryCollection())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { + 'collection with page-based pagination enabled' => [\stdClass::class, (new QueryCollection())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName')->withPaginationType('page')->withFilters(['my_filter']), [], $graphqlType = GraphQLType::listOf(new ObjectType(['name' => 'collection', 'fields' => []])), $resolver = function (): void { }, [ 'actionShortNames' => [ @@ -340,7 +328,7 @@ public static function collectionQueryFieldsProvider(): array 'type' => GraphQLType::int(), 'description' => 'Returns the current page.', ], - 'boolField' => $graphqlType, + 'boolField' => GraphQLType::nonNull($graphqlType), 'boolField_list' => GraphQLType::listOf($graphqlType), 'parent__child' => GraphQLType::listOf(new InputObjectType(['name' => 'ShortNameFilter_parent__child', 'fields' => ['related__nested' => $graphqlType]])), 'dateField' => GraphQLType::listOf(new InputObjectType(['name' => 'ShortNameFilter_dateField', 'fields' => ['before' => $graphqlType]])), @@ -353,16 +341,13 @@ public static function collectionQueryFieldsProvider(): array ]; } - /** - * @dataProvider mutationFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('mutationFieldsProvider')] public function testGetMutationFields(string $resourceClass, Operation $operation, GraphQLType $graphqlType, GraphQLType $inputGraphqlType, ?callable $mutationResolver, array $expectedMutationFields): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); - $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); - $this->itemMutationResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($mutationResolver); + $this->typeConverterProphecy->convertPhpType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); + $this->typeConverterProphecy->convertPhpType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); + $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation, Argument::any())->willReturn($mutationResolver); $mutationFields = $this->fieldsBuilder->getMutationFields($resourceClass, $operation); @@ -372,7 +357,7 @@ public function testGetMutationFields(string $resourceClass, Operation $operatio public static function mutationFieldsProvider(): array { return [ - 'nominal case with deprecation reason' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $mutationResolver = function (): void { + 'nominal case with deprecation reason' => [\stdClass::class, (new Mutation())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName')->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'mutation', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -392,7 +377,7 @@ public static function mutationFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $mutationResolver = function (): void { + 'custom description' => [\stdClass::class, (new Mutation())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName')->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'mutation', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $mutationResolver = function (): void { }, [ 'actionShortName' => [ @@ -415,17 +400,14 @@ public static function mutationFieldsProvider(): array ]; } - /** - * @dataProvider subscriptionFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('subscriptionFieldsProvider')] public function testGetSubscriptionFields(string $resourceClass, Operation $operation, GraphQLType $graphqlType, GraphQLType $inputGraphqlType, ?callable $subscriptionResolver, array $expectedSubscriptionFields): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); - $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); + $this->typeConverterProphecy->convertPhpType(Argument::type(Type::class), false, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($graphqlType); + $this->typeConverterProphecy->convertPhpType(Argument::type(Type::class), true, Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), $resourceClass, $resourceClass, null, 0)->willReturn($inputGraphqlType); $this->resourceMetadataCollectionFactoryProphecy->create($resourceClass)->willReturn(new ResourceMetadataCollection($resourceClass, [(new ApiResource())->withGraphQlOperations([$operation->getName() => $operation])])); - $this->itemSubscriptionResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation)->willReturn($subscriptionResolver); + $this->itemResolverFactoryProphecy->__invoke($resourceClass, $resourceClass, $operation, Argument::any())->willReturn($subscriptionResolver); $subscriptionFields = $this->fieldsBuilder->getSubscriptionFields($resourceClass, $operation); @@ -435,9 +417,9 @@ public function testGetSubscriptionFields(string $resourceClass, Operation $oper public static function subscriptionFieldsProvider(): array { return [ - 'mercure not enabled' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription', 'fields' => []]), new ObjectType(['name' => 'input', 'fields' => []]), null, [], + 'mercure not enabled' => [\stdClass::class, (new Subscription())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName'), new ObjectType(['name' => 'subscription', 'fields' => []]), new ObjectType(['name' => 'input', 'fields' => []]), null, [], ], - 'nominal case with deprecation reason' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $subscriptionResolver = function (): void { + 'nominal case with deprecation reason' => [\stdClass::class, (new Subscription())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName')->withMercure(true)->withDeprecationReason('not useful'), $graphqlType = new ObjectType(['name' => 'subscription', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -457,7 +439,7 @@ public static function subscriptionFieldsProvider(): array ], ], ], - 'custom description' => ['resourceClass', (new Subscription())->withClass('resourceClass')->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $subscriptionResolver = function (): void { + 'custom description' => [\stdClass::class, (new Subscription())->withClass(\stdClass::class)->withName('action')->withShortName('ShortName')->withMercure(true)->withDescription('Custom description.'), $graphqlType = new ObjectType(['name' => 'subscription', 'fields' => []]), $inputGraphqlType = new ObjectType(['name' => 'input', 'fields' => []]), $subscriptionResolver = function (): void { }, [ 'actionShortNameSubscribe' => [ @@ -480,9 +462,7 @@ public static function subscriptionFieldsProvider(): array ]; } - /** - * @dataProvider resourceObjectTypeFieldsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('resourceObjectTypeFieldsProvider')] public function testGetResourceObjectTypeFields(string $resourceClass, Operation $operation, array $properties, bool $input, int $depth, ?array $ioMetadata, array $expectedResourceObjectTypeFields, ?callable $advancedNameConverterFactory = null): void { $this->resourceClassResolverProphecy->isResourceClass($resourceClass)->willReturn(true); @@ -492,27 +472,26 @@ public function testGetResourceObjectTypeFields(string $resourceClass, Operation $this->propertyNameCollectionFactoryProphecy->create($resourceClass)->willReturn(new PropertyNameCollection(array_keys($properties))); foreach ($properties as $propertyName => $propertyMetadata) { $this->propertyMetadataFactoryProphecy->create($resourceClass, $propertyName, ['normalization_groups' => null, 'denormalization_groups' => null])->willReturn($propertyMetadata); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_NULL), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(null); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_CALLABLE), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn('NotRegisteredType'); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::string()); - $this->typeConverterProphecy->convertType(new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); + $this->typeConverterProphecy->convertPhpType(Type::null(), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(null); + $this->typeConverterProphecy->convertPhpType(Type::callable(), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn('NotRegisteredType'); + $this->typeConverterProphecy->convertPhpType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::string()); + $this->typeConverterProphecy->convertPhpType(Type::list(Type::string()), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), '', $resourceClass, $propertyName, $depth + 1)->willReturn(GraphQLType::nonNull(GraphQLType::listOf(GraphQLType::nonNull(GraphQLType::string())))); if ('propertyObject' === $propertyName) { - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType', 'fields' => []])); - $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation)->willReturn(static function (): void { + $this->typeConverterProphecy->convertPhpType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'objectClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType', 'fields' => []])); + $this->itemResolverFactoryProphecy->__invoke('objectClass', $resourceClass, $operation, Argument::any())->willReturn(static function (): void { }); } if ('propertyNestedResource' === $propertyName) { $nestedResourceQueryOperation = new Query(); $this->resourceMetadataCollectionFactoryProphecy->create('nestedResourceClass')->willReturn(new ResourceMetadataCollection('nestedResourceClass', [(new ApiResource())->withGraphQlOperations(['item_query' => $nestedResourceQueryOperation])])); - $this->typeConverterProphecy->convertType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType', 'fields' => []])); - $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation)->willReturn(static function (): void { + $this->typeConverterProphecy->convertPhpType(Argument::type(Type::class), Argument::type('bool'), Argument::that(static fn (Operation $arg): bool => $arg->getName() === $operation->getName()), 'nestedResourceClass', $resourceClass, $propertyName, $depth + 1)->willReturn(new ObjectType(['name' => 'objectType', 'fields' => []])); + $this->itemResolverFactoryProphecy->__invoke('nestedResourceClass', $resourceClass, $nestedResourceQueryOperation, Argument::any())->willReturn(static function (): void { }); } } $this->typesContainerProphecy->has('NotRegisteredType')->willReturn(false); $this->typesContainerProphecy->all()->willReturn([]); - $this->typeBuilderProphecy->isCollection(Argument::type(Type::class))->willReturn(false); $fieldsBuilder = $this->fieldsBuilder; if ($advancedNameConverterFactory) { @@ -527,17 +506,17 @@ public static function resourceObjectTypeFieldsProvider(): iterable { $advancedNameConverterFactory = function (self $that): AdvancedNameConverterInterface { $advancedNameConverterProphecy = $that->prophesize(AdvancedNameConverterInterface::class); - $advancedNameConverterProphecy->normalize('field', 'resourceClass')->willReturn('normalizedField'); + $advancedNameConverterProphecy->normalize('field', \stdClass::class)->willReturn('normalizedField'); return $advancedNameConverterProphecy->reveal(); }; - yield 'query' => ['resourceClass', (new Query())->withClass('resourceClass'), + yield 'query' => [\stdClass::class, (new Query())->withClass(\stdClass::class), [ 'property' => new ApiProperty(), - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(false), - 'propertyNotReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(false), - 'nameConverted' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(false), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(true)->withWritable(false), + 'propertyNotReadable' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(false), + 'nameConverted' => (new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withWritable(false), ], false, 0, null, [ @@ -560,9 +539,9 @@ public static function resourceObjectTypeFieldsProvider(): iterable ], ], ]; - yield 'query with advanced name converter' => ['resourceClass', (new Query())->withClass('resourceClass'), + yield 'query with advanced name converter' => [\stdClass::class, (new Query())->withClass(\stdClass::class), [ - 'field' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(true)->withWritable(false), + 'field' => (new ApiProperty())->withNativeType(Type::string())->withReadable(true)->withWritable(false), ], false, 0, null, [ @@ -579,11 +558,11 @@ public static function resourceObjectTypeFieldsProvider(): iterable ], $advancedNameConverterFactory, ]; - yield 'query input' => ['resourceClass', (new Query())->withClass('resourceClass'), + yield 'query input' => [\stdClass::class, (new Query())->withClass(\stdClass::class), [ 'property' => new ApiProperty(), - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), - 'nonWritableProperty' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(false)->withWritable(false), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), + 'nonWritableProperty' => (new ApiProperty())->withNativeType(Type::string())->withReadable(false)->withWritable(false), ], true, 0, null, [ @@ -599,11 +578,9 @@ public static function resourceObjectTypeFieldsProvider(): iterable ], ], ]; - yield 'query with simple non-null string array property' => ['resourceClass', (new Query())->withClass('resourceClass'), + yield 'query with simple non-null string array property' => [\stdClass::class, (new Query())->withClass(\stdClass::class), [ - 'property' => (new ApiProperty())->withBuiltinTypes([ - new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING)), - ])->withReadable(true)->withWritable(false), + 'property' => (new ApiProperty())->withNativeType(Type::list(Type::string()))->withReadable(true)->withWritable(false), ], false, 0, null, [ @@ -619,9 +596,9 @@ public static function resourceObjectTypeFieldsProvider(): iterable ], ], ]; - yield 'query with nested resources' => ['resourceClass', (new Query())->withClass('resourceClass'), + yield 'query with nested resources' => [\stdClass::class, (new Query())->withClass(\stdClass::class), [ - 'propertyNestedResource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'nestedResourceClass')])->withReadable(true)->withWritable(true), + 'propertyNestedResource' => (new ApiProperty())->withNativeType(Type::object('nestedResourceClass'))->withReadable(true)->withWritable(true), ], false, 0, null, [ @@ -638,12 +615,12 @@ public static function resourceObjectTypeFieldsProvider(): iterable ], ], ]; - yield 'mutation non input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + yield 'mutation non input' => [\stdClass::class, (new Mutation())->withClass(\stdClass::class)->withName('mutation'), [ 'property' => new ApiProperty(), - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), - 'propertyReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(true), - 'propertyObject' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, 'objectClass')])->withReadable(true)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), + 'propertyReadable' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(true)->withWritable(true), + 'propertyObject' => (new ApiProperty())->withNativeType(Type::object('objectClass'))->withReadable(true)->withWritable(true), ], false, 0, null, [ @@ -667,13 +644,13 @@ public static function resourceObjectTypeFieldsProvider(): iterable ], ], ]; - yield 'mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + yield 'mutation input' => [\stdClass::class, (new Mutation())->withClass(\stdClass::class)->withName('mutation'), [ 'property' => new ApiProperty(), - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), - 'propertySubresource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), - 'nonWritableProperty' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withReadable(false)->withWritable(false), - 'id' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withReadable(false)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), + 'propertySubresource' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), + 'nonWritableProperty' => (new ApiProperty())->withNativeType(Type::string())->withReadable(false)->withWritable(false), + 'id' => (new ApiProperty())->withNativeType(Type::int())->withReadable(false)->withWritable(true), ], true, 0, null, [ @@ -704,9 +681,9 @@ public static function resourceObjectTypeFieldsProvider(): iterable 'clientMutationId' => GraphQLType::string(), ], ]; - yield 'custom mutation' => ['resourceClass', (new Mutation())->withResolver('resolver')->withName('mutation'), + yield 'custom mutation' => [\stdClass::class, (new Mutation())->withResolver('resolver')->withName('mutation'), [ - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withDescription('propertyBool description')->withReadable(false)->withWritable(true), ], true, 0, null, [ @@ -720,9 +697,9 @@ public static function resourceObjectTypeFieldsProvider(): iterable 'clientMutationId' => GraphQLType::string(), ], ]; - yield 'mutation nested input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('mutation'), + yield 'mutation nested input' => [\stdClass::class, (new Mutation())->withClass(\stdClass::class)->withName('mutation'), [ - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), ], true, 1, null, [ @@ -739,9 +716,9 @@ public static function resourceObjectTypeFieldsProvider(): iterable 'clientMutationId' => GraphQLType::string(), ], ]; - yield 'delete mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('delete'), + yield 'delete mutation input' => [\stdClass::class, (new Mutation())->withClass(\stdClass::class)->withName('delete'), [ - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), ], true, 0, null, [ @@ -751,9 +728,9 @@ public static function resourceObjectTypeFieldsProvider(): iterable 'clientMutationId' => GraphQLType::string(), ], ]; - yield 'create mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('create'), + yield 'create mutation input' => [\stdClass::class, (new Mutation())->withClass(\stdClass::class)->withName('create'), [ - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), ], true, 0, null, [ @@ -767,9 +744,9 @@ public static function resourceObjectTypeFieldsProvider(): iterable 'clientMutationId' => GraphQLType::string(), ], ]; - yield 'update mutation input' => ['resourceClass', (new Mutation())->withClass('resourceClass')->withName('update'), + yield 'update mutation input' => [\stdClass::class, (new Mutation())->withClass(\stdClass::class)->withName('update'), [ - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), ], true, 0, null, [ @@ -786,11 +763,11 @@ public static function resourceObjectTypeFieldsProvider(): iterable 'clientMutationId' => GraphQLType::string(), ], ]; - yield 'subscription non input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), + yield 'subscription non input' => [\stdClass::class, (new Subscription())->withClass(\stdClass::class), [ 'property' => new ApiProperty(), - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), - 'propertyReadable' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(true)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), + 'propertyReadable' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(true)->withWritable(true), ], false, 0, null, [ @@ -806,12 +783,12 @@ public static function resourceObjectTypeFieldsProvider(): iterable ], ], ]; - yield 'subscription input' => ['resourceClass', (new Subscription())->withClass('resourceClass'), + yield 'subscription input' => [\stdClass::class, (new Subscription())->withClass(\stdClass::class), [ 'property' => new ApiProperty(), - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), - 'propertySubresource' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), - 'id' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withReadable(false)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withDescription('propertyBool description')->withReadable(false)->withWritable(true)->withDeprecationReason('not useful'), + 'propertySubresource' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), + 'id' => (new ApiProperty())->withNativeType(Type::int())->withReadable(false)->withWritable(true), ], true, 0, null, [ @@ -821,25 +798,25 @@ public static function resourceObjectTypeFieldsProvider(): iterable 'clientSubscriptionId' => GraphQLType::string(), ], ]; - yield 'null io metadata non input' => ['resourceClass', (new Query())->withClass('resourceClass'), + yield 'null io metadata non input' => [\stdClass::class, (new Query())->withClass(\stdClass::class), [ - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), ], false, 0, ['class' => null], [], ]; - yield 'null io metadata input' => ['resourceClass', (new Query())->withClass('resourceClass'), + yield 'null io metadata input' => [\stdClass::class, (new Query())->withClass(\stdClass::class), [ - 'propertyBool' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withReadable(false)->withWritable(true), + 'propertyBool' => (new ApiProperty())->withNativeType(Type::bool())->withReadable(false)->withWritable(true), ], true, 0, ['class' => null], [ 'clientMutationId' => GraphQLType::string(), ], ]; - yield 'invalid types' => ['resourceClass', (new Query())->withClass('resourceClass'), + yield 'invalid types' => [\stdClass::class, (new Query())->withClass(\stdClass::class), [ - 'propertyInvalidType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_NULL)])->withReadable(true)->withWritable(false), - 'propertyNotRegisteredType' => (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_CALLABLE)])->withReadable(true)->withWritable(false), + 'propertyInvalidType' => (new ApiProperty())->withNativeType(Type::null())->withReadable(true)->withWritable(false), + 'propertyNotRegisteredType' => (new ApiProperty())->withNativeType(Type::callable())->withReadable(true)->withWritable(false), ], false, 0, null, [ @@ -869,9 +846,7 @@ public function testGetEnumFields(): void ], $enumFields); } - /** - * @dataProvider resolveResourceArgsProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('resolveResourceArgsProvider')] public function testResolveResourceArgs(array $args, array $expectedResolvedArgs, ?string $expectedExceptionMessage = null): void { if (null !== $expectedExceptionMessage) { @@ -880,7 +855,6 @@ public function testResolveResourceArgs(array $args, array $expectedResolvedArgs $this->typeConverterProphecy->resolveType(Argument::type('string'))->willReturn(GraphQLType::string()); - /** @var Operation $operation */ $operation = (new Query())->withName('operation')->withShortName('shortName'); $args = $this->fieldsBuilder->resolveResourceArgs($args, $operation); diff --git a/src/GraphQl/Tests/Type/SchemaBuilderTest.php b/src/GraphQl/Tests/Type/SchemaBuilderTest.php index 8428663f50f..13ef9fa04b1 100644 --- a/src/GraphQl/Tests/Type/SchemaBuilderTest.php +++ b/src/GraphQl/Tests/Type/SchemaBuilderTest.php @@ -62,9 +62,7 @@ protected function setUp(): void $this->schemaBuilder = new SchemaBuilder($this->resourceNameCollectionFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->typesFactoryProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->fieldsBuilderProphecy->reveal()); } - /** - * @dataProvider schemaProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('schemaProvider')] public function testGetSchema(string $resourceClass, ResourceMetadataCollection $resourceMetadata, ObjectType $expectedQueryType, ?ObjectType $expectedMutationType, ?ObjectType $expectedSubscriptionType): void { $type = new StringType(['name' => 'MyType']); diff --git a/src/GraphQl/Tests/Type/TypeBuilderTest.php b/src/GraphQl/Tests/Type/TypeBuilderTest.php index 11a5f2ae7d6..2c6dbe5ad3b 100644 --- a/src/GraphQl/Tests/Type/TypeBuilderTest.php +++ b/src/GraphQl/Tests/Type/TypeBuilderTest.php @@ -36,12 +36,15 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type as GraphQLType; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Psr\Container\ContainerInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * @author Alan Poulain @@ -75,7 +78,7 @@ protected function setUp(): void public function testGetResourceObjectType(): void { - $resourceMetadataCollection = new ResourceMetadataCollection('resourceClass', [ + $resourceMetadataCollection = new ResourceMetadataCollection(\stdClass::class, [ (new ApiResource())->withGraphQlOperations(['collection_query' => new QueryCollection()]), ]); $this->typesContainerProphecy->has('shortName')->shouldBeCalled()->willReturn(false); @@ -83,8 +86,7 @@ public function testGetResourceObjectType(): void $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Query())->withShortName('shortName')->withDescription('description')->withClass('resourceClass'); + $operation = (new Query())->withShortName('shortName')->withDescription('description')->withClass(\stdClass::class); /** @var ObjectType $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadataCollection, $operation, null, ['input' => false]); $this->assertSame('shortName', $resourceObjectType->name); @@ -94,14 +96,14 @@ public function testGetResourceObjectType(): void $this->assertArrayHasKey('fields', $resourceObjectType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields(\stdClass::class, $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); } public function testGetResourceObjectTypeOutputClass(): void { - $resourceMetadataCollection = new ResourceMetadataCollection('resourceClass', [ + $resourceMetadataCollection = new ResourceMetadataCollection(\stdClass::class, [ (new ApiResource())->withGraphQlOperations(['collection_query' => new QueryCollection()]), ]); $this->typesContainerProphecy->has('shortName')->shouldBeCalled()->willReturn(false); @@ -109,7 +111,6 @@ public function testGetResourceObjectTypeOutputClass(): void $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ $operation = (new Query())->withShortName('shortName')->withDescription('description')->withOutput(['class' => 'outputClass']); /** @var ObjectType $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadataCollection, $operation, null, ['input' => false]); @@ -125,12 +126,10 @@ public function testGetResourceObjectTypeOutputClass(): void $resourceObjectType->config['fields'](); } - /** - * @dataProvider resourceObjectTypeQuerySerializationGroupsProvider - */ + #[DataProvider('resourceObjectTypeQuerySerializationGroupsProvider')] public function testGetResourceObjectTypeQuerySerializationGroups(string $itemSerializationGroup, string $collectionSerializationGroup, Operation $operation, string $shortName): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, [(new ApiResource())->withGraphQlOperations([ 'item_query' => (new Query())->withShortName('shortName')->withNormalizationContext(['groups' => [$itemSerializationGroup]]), 'collection_query' => (new QueryCollection())->withShortName('shortName')->withNormalizationContext(['groups' => [$collectionSerializationGroup]]), ])]); @@ -170,14 +169,13 @@ public static function resourceObjectTypeQuerySerializationGroupsProvider(): arr public function testGetResourceObjectTypeInput(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, []); $this->typesContainerProphecy->has('customShortNameInput')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('customShortNameInput', Argument::type(InputObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Mutation())->withName('custom')->withShortName('shortName')->withDescription('description')->withClass('resourceClass'); + $operation = (new Mutation())->withName('custom')->withShortName('shortName')->withDescription('description')->withClass(\stdClass::class); /** @var NonNull $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadata, $operation, null, ['input' => true]); /** @var InputObjectType $wrappedType */ @@ -188,21 +186,20 @@ public function testGetResourceObjectTypeInput(): void $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 0, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields(\stdClass::class, $operation, true, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); } public function testGetResourceObjectTypeNestedInput(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, []); $this->typesContainerProphecy->has('customShortNameNestedInput')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('customShortNameNestedInput', Argument::type(InputObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Mutation())->withName('custom')->withShortName('shortName')->withDescription('description')->withClass('resourceClass'); + $operation = (new Mutation())->withName('custom')->withShortName('shortName')->withDescription('description')->withClass(\stdClass::class); /** @var NonNull $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadata, $operation, null, ['input' => true, 'wrapped' => false, 'depth' => 1]); /** @var InputObjectType $wrappedType */ @@ -213,22 +210,20 @@ public function testGetResourceObjectTypeNestedInput(): void $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 1, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields(\stdClass::class, $operation, true, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); } public function testGetResourceObjectTypeNestedInputNullable(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, []); $this->typesContainerProphecy->has('customShortNameNullableNestedInput')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('customShortNameNullableNestedInput', Argument::type(InputObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Mutation())->withName('custom')->withShortName('shortNameNullable')->withDescription('description nullable')->withClass('resourceClass'); - /** @var ApiProperty $propertyMetadata */ + $operation = (new Mutation())->withName('custom')->withShortName('shortNameNullable')->withDescription('description nullable')->withClass(\stdClass::class); $propertyMetadata = (new ApiProperty())->withRequired(false); /** @var InputObjectType $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadata, $operation, $propertyMetadata, [ @@ -243,21 +238,20 @@ public function testGetResourceObjectTypeNestedInputNullable(): void $this->assertArrayHasKey('fields', $resourceObjectType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 1, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields(\stdClass::class, $operation, true, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); } public function testGetResourceObjectTypeCustomMutationInputArgs(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, []); $this->typesContainerProphecy->has('customShortNameInput')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('customShortNameInput', Argument::type(InputObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Mutation())->withArgs([])->withName('custom')->withShortName('shortName')->withDescription('description')->withClass('resourceClass'); + $operation = (new Mutation())->withArgs([])->withName('custom')->withShortName('shortName')->withDescription('description')->withClass(\stdClass::class); /** @var NonNull $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadata, $operation, null, ['input' => true]); /** @var InputObjectType $wrappedType */ @@ -268,7 +262,7 @@ public function testGetResourceObjectTypeCustomMutationInputArgs(): void $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, true, 0, null) + $fieldsBuilderProphecy->getResourceObjectTypeFields(\stdClass::class, $operation, true, 0, null) ->shouldBeCalled()->willReturn(['clientMutationId' => GraphQLType::string()]); $fieldsBuilderProphecy->resolveResourceArgs([], $operation)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); @@ -277,7 +271,7 @@ public function testGetResourceObjectTypeCustomMutationInputArgs(): void public function testGetResourceObjectTypeMutation(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, [(new ApiResource())->withGraphQlOperations([ 'create' => (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description'), 'item_query' => (new Query())->withShortName('shortName')->withDescription('description'), 'collection_query' => new QueryCollection(), @@ -287,8 +281,7 @@ public function testGetResourceObjectTypeMutation(): void $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description')->withClass('resourceClass'); + $operation = (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description')->withClass(\stdClass::class); /** @var ObjectType $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadata, $operation, null, ['input' => false]); $this->assertSame('createShortNamePayload', $resourceObjectType->name); @@ -310,17 +303,16 @@ public function testGetResourceObjectTypeMutation(): void public function testGetResourceObjectTypeMutationWrappedType(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ - 'item_query' => (new Query())->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['item_query']])->withClass('resourceClass'), - 'create' => (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['create']])->withClass('resourceClass'), + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, [(new ApiResource())->withGraphQlOperations([ + 'item_query' => (new Query())->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['item_query']])->withClass(\stdClass::class), + 'create' => (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['create']])->withClass(\stdClass::class), ])]); $this->typesContainerProphecy->has('createShortNamePayload')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('createShortNamePayload', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['create']])->withClass('resourceClass'); + $operation = (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['create']])->withClass(\stdClass::class); /** @var ObjectType $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadata, $operation, null, ['input' => false]); $this->assertSame('createShortNamePayload', $resourceObjectType->name); @@ -348,21 +340,20 @@ public function testGetResourceObjectTypeMutationWrappedType(): void $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields(\stdClass::class, $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); } public function testGetResourceObjectTypeMutationNested(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', []); + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, []); $this->typesContainerProphecy->has('createShortNameNestedPayload')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('createShortNameNestedPayload', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description')->withClass('resourceClass'); + $operation = (new Mutation())->withName('create')->withShortName('shortName')->withDescription('description')->withClass(\stdClass::class); /** @var ObjectType $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadata, $operation, null, ['input' => false, 'wrapped' => false, 'depth' => 1]); $this->assertSame('createShortNameNestedPayload', $resourceObjectType->name); @@ -372,14 +363,14 @@ public function testGetResourceObjectTypeMutationNested(): void $this->assertArrayHasKey('fields', $resourceObjectType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 1, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields(\stdClass::class, $operation, false, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); } public function testGetResourceObjectTypeSubscription(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, [(new ApiResource())->withGraphQlOperations([ 'update' => (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withMercure(true), 'item_query' => (new Query())->withShortName('shortName')->withDescription('description'), 'collection_query' => new QueryCollection(), @@ -389,8 +380,7 @@ public function testGetResourceObjectTypeSubscription(): void $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withMercure(true)->withClass('resourceClass'); + $operation = (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withMercure(true)->withClass(\stdClass::class); /** @var ObjectType $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadata, $operation, null, ['input' => false]); $this->assertSame('updateShortNameSubscriptionPayload', $resourceObjectType->name); @@ -414,17 +404,16 @@ public function testGetResourceObjectTypeSubscription(): void public function testGetResourceObjectTypeSubscriptionWrappedType(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([ - 'item_query' => (new Query())->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['item_query']])->withClass('resourceClass'), - 'update' => (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['update']])->withClass('resourceClass'), + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, [(new ApiResource())->withGraphQlOperations([ + 'item_query' => (new Query())->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['item_query']])->withClass(\stdClass::class), + 'update' => (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['update']])->withClass(\stdClass::class), ])]); $this->typesContainerProphecy->has('updateShortNameSubscriptionPayload')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('updateShortNameSubscriptionPayload', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['update']])->withClass('resourceClass'); + $operation = (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withNormalizationContext(['groups' => ['update']])->withClass(\stdClass::class); /** @var ObjectType $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadata, $operation, null, ['input' => false]); $this->assertSame('updateShortNameSubscriptionPayload', $resourceObjectType->name); @@ -453,21 +442,20 @@ public function testGetResourceObjectTypeSubscriptionWrappedType(): void $this->assertArrayHasKey('fields', $wrappedType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 0, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields(\stdClass::class, $operation, false, 0, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $wrappedType->config['fields'](); } public function testGetResourceObjectTypeSubscriptionNested(): void { - $resourceMetadata = new ResourceMetadataCollection('resourceClass', [(new ApiResource())->withGraphQlOperations([])]); + $resourceMetadata = new ResourceMetadataCollection(\stdClass::class, [(new ApiResource())->withGraphQlOperations([])]); $this->typesContainerProphecy->has('updateShortNameSubscriptionNestedPayload')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('updateShortNameSubscriptionNestedPayload', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->has('Node')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('Node', Argument::type(InterfaceType::class))->shouldBeCalled(); - /** @var Operation $operation */ - $operation = (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withMercure(true)->withClass('resourceClass'); + $operation = (new Subscription())->withName('update')->withShortName('shortName')->withDescription('description')->withMercure(true)->withClass(\stdClass::class); /** @var ObjectType $resourceObjectType */ $resourceObjectType = $this->typeBuilder->getResourceObjectType($resourceMetadata, $operation, null, ['input' => false, 'wrapped' => false, 'depth' => 1]); $this->assertSame('updateShortNameSubscriptionNestedPayload', $resourceObjectType->name); @@ -477,7 +465,7 @@ public function testGetResourceObjectTypeSubscriptionNested(): void $this->assertArrayHasKey('fields', $resourceObjectType->config); $fieldsBuilderProphecy = $this->prophesize(FieldsBuilderEnumInterface::class); - $fieldsBuilderProphecy->getResourceObjectTypeFields('resourceClass', $operation, false, 1, null)->shouldBeCalled(); + $fieldsBuilderProphecy->getResourceObjectTypeFields(\stdClass::class, $operation, false, 1, null)->shouldBeCalled(); $this->fieldsBuilderLocatorProphecy->get('api_platform.graphql.fields_builder')->shouldBeCalled()->willReturn($fieldsBuilderProphecy->reveal()); $resourceObjectType->config['fields'](); } @@ -495,18 +483,17 @@ public function testGetNodeInterface(): void $this->assertNull($nodeInterface->resolveType([], [], $this->prophesize(ResolveInfo::class)->reveal())); $this->typesContainerProphecy->has('Dummy')->shouldBeCalled()->willReturn(false); - $this->assertNull($nodeInterface->resolveType([ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => Dummy::class], [], $this->prophesize(ResolveInfo::class)->reveal())); + $resolvedType = $nodeInterface->resolveType([ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => Dummy::class], [], $this->prophesize(ResolveInfo::class)->reveal()); + $this->assertNull($resolvedType); $this->typesContainerProphecy->has('Dummy')->shouldBeCalled()->willReturn(true); $this->typesContainerProphecy->get('Dummy')->shouldBeCalled()->willReturn(GraphQLType::string()); - /** @var GraphQLType $resolvedType */ $resolvedType = $nodeInterface->resolveType([ItemNormalizer::ITEM_RESOURCE_CLASS_KEY => Dummy::class], [], $this->prophesize(ResolveInfo::class)->reveal()); $this->assertSame(GraphQLType::string(), $resolvedType); } public function testCursorBasedGetPaginatedCollectionType(): void { - /** @var Operation $operation */ $operation = (new Query())->withPaginationType('cursor'); $this->typesContainerProphecy->has('StringCursorConnection')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('StringCursorConnection', Argument::type(ObjectType::class))->shouldBeCalled(); @@ -562,7 +549,6 @@ public function testCursorBasedGetPaginatedCollectionType(): void public function testPageBasedGetPaginatedCollectionType(): void { - /** @var Operation $operation */ $operation = (new Query())->withPaginationType('page'); $this->typesContainerProphecy->has('StringPageConnection')->shouldBeCalled()->willReturn(false); $this->typesContainerProphecy->set('StringPageConnection', Argument::type(ObjectType::class))->shouldBeCalled(); @@ -600,7 +586,6 @@ public function testGetEnumType(): void $enumClass = GamePlayMode::class; $enumName = 'GamePlayMode'; $enumDescription = 'GamePlayMode description'; - /** @var Operation $operation */ $operation = (new Operation()) ->withClass($enumClass) ->withShortName($enumName) @@ -624,26 +609,40 @@ public function testGetEnumType(): void ]), $this->typeBuilder->getEnumType($operation)); } - /** - * @dataProvider typesProvider - */ - public function testIsCollection(Type $type, bool $expectedIsCollection): void + #[DataProvider('legacyTypesProvider')] + #[IgnoreDeprecations] + public function testIsCollectionLegacy(LegacyType $type, bool $expectedIsCollection): void { + $this->expectUserDeprecationMessage('Since api-platform/graphql 4.2: The "ApiPlatform\GraphQl\Type\TypeBuilder::isCollection()" method is deprecated and will be removed.'); $this->assertSame($expectedIsCollection, $this->typeBuilder->isCollection($type)); } + public static function legacyTypesProvider(): array + { + return [ + [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL), false], + [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT), false], + [new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE, false, null, false), false], + [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, null, true), false], + [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true), false], + [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, null, new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT)), false], + [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'className', true), false], + [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, null, true, null, new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'className')), true], + [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, null, new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'className')), true], + ]; + } + public static function typesProvider(): array { return [ - [new Type(Type::BUILTIN_TYPE_BOOL), false], - [new Type(Type::BUILTIN_TYPE_OBJECT), false], - [new Type(Type::BUILTIN_TYPE_RESOURCE, false, null, false), false], - [new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true), false], - [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true), false], - [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT)), false], - [new Type(Type::BUILTIN_TYPE_OBJECT, false, 'className', true), false], - [new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'className')), true], - [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'className')), true], + [Type::bool(), false], + [Type::object(), false], + [Type::resource(), false], + [Type::collection(Type::object(\Stringable::class)), false], + [Type::array(), false], + [Type::array(Type::object()), false], + [Type::collection(Type::object(\Traversable::class), Type::object(\Stringable::class)), true], + [Type::array(Type::object(\Stringable::class)), true], ]; } } diff --git a/src/GraphQl/Tests/Type/TypeConverterTest.php b/src/GraphQl/Tests/Type/TypeConverterTest.php index 15ab234b8a9..fe6e4c96e76 100644 --- a/src/GraphQl/Tests/Type/TypeConverterTest.php +++ b/src/GraphQl/Tests/Type/TypeConverterTest.php @@ -30,11 +30,14 @@ use GraphQL\Type\Definition\EnumType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type as GraphQLType; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * @author Alan Poulain @@ -61,55 +64,96 @@ protected function setUp(): void $this->typeConverter = new TypeConverter($this->typeBuilderProphecy->reveal(), $this->typesContainerProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal()); } - /** - * @dataProvider convertTypeProvider - */ - public function testConvertType(Type $type, bool $input, int $depth, GraphQLType|string|null $expectedGraphqlType): void + #[DataProvider('legacyConvertTypeProvider')] + #[IgnoreDeprecations] + public function testConvertTypeLegacy(LegacyType $type, bool $input, int $depth, GraphQLType|string|null $expectedGraphqlType): void { + $this->expectUserDeprecationMessage('Since api-platform/graphql 4.2: The "ApiPlatform\GraphQl\Type\TypeConverter::convertType()" method is deprecated, use "ApiPlatform\GraphQl\Type\TypeConverter::convertPhpType()" instead.'); + $this->typeBuilderProphecy->isCollection($type)->willReturn(false); $this->resourceMetadataCollectionFactoryProphecy->create(Argument::type('string'))->willReturn(new ResourceMetadataCollection('resourceClass')); $this->typeBuilderProphecy->getEnumType(Argument::type(Operation::class))->willReturn($expectedGraphqlType); - /** @var Operation $operation */ $operation = (new Query())->withName('test'); $graphqlType = $this->typeConverter->convertType($type, $input, $operation, 'resourceClass', 'rootClass', null, $depth); $this->assertSame($expectedGraphqlType, $graphqlType); } + public static function legacyConvertTypeProvider(): array + { + return [ + [new LegacyType(LegacyType::BUILTIN_TYPE_BOOL), false, 0, GraphQLType::boolean()], + [new LegacyType(LegacyType::BUILTIN_TYPE_INT), false, 0, GraphQLType::int()], + [new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT), false, 0, GraphQLType::float()], + [new LegacyType(LegacyType::BUILTIN_TYPE_STRING), false, 0, GraphQLType::string()], + [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY), false, 0, 'Iterable'], + [new LegacyType(LegacyType::BUILTIN_TYPE_ITERABLE), false, 0, 'Iterable'], + [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, \DateTimeInterface::class), false, 0, GraphQLType::string()], + [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class), false, 0, new EnumType(['name' => 'GenderTypeEnum', 'values' => []])], + [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT), false, 0, null], + [new LegacyType(LegacyType::BUILTIN_TYPE_CALLABLE), false, 0, null], + [new LegacyType(LegacyType::BUILTIN_TYPE_NULL), false, 0, null], + [new LegacyType(LegacyType::BUILTIN_TYPE_RESOURCE), false, 0, null], + ]; + } + + #[DataProvider('convertTypeProvider')] + public function testConvertType(Type $type, bool $input, int $depth, GraphQLType|string|null $expectedGraphqlType): void + { + $this->resourceMetadataCollectionFactoryProphecy->create(Argument::type('string'))->willReturn(new ResourceMetadataCollection('resourceClass')); + $this->typeBuilderProphecy->getEnumType(Argument::type(Operation::class))->willReturn($expectedGraphqlType); + + $operation = (new Query())->withName('test'); + $graphqlType = $this->typeConverter->convertPhpType($type, $input, $operation, 'resourceClass', 'rootClass', null, $depth); + $this->assertSame($expectedGraphqlType, $graphqlType); + } + public static function convertTypeProvider(): array { return [ - [new Type(Type::BUILTIN_TYPE_BOOL), false, 0, GraphQLType::boolean()], - [new Type(Type::BUILTIN_TYPE_INT), false, 0, GraphQLType::int()], - [new Type(Type::BUILTIN_TYPE_FLOAT), false, 0, GraphQLType::float()], - [new Type(Type::BUILTIN_TYPE_STRING), false, 0, GraphQLType::string()], - [new Type(Type::BUILTIN_TYPE_ARRAY), false, 0, 'Iterable'], - [new Type(Type::BUILTIN_TYPE_ITERABLE), false, 0, 'Iterable'], - [new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeInterface::class), false, 0, GraphQLType::string()], - [new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class), false, 0, new EnumType(['name' => 'GenderTypeEnum', 'values' => []])], - [new Type(Type::BUILTIN_TYPE_OBJECT), false, 0, null], - [new Type(Type::BUILTIN_TYPE_CALLABLE), false, 0, null], - [new Type(Type::BUILTIN_TYPE_NULL), false, 0, null], - [new Type(Type::BUILTIN_TYPE_RESOURCE), false, 0, null], + [Type::bool(), false, 0, GraphQLType::boolean()], + [Type::int(), false, 0, GraphQLType::int()], + [Type::float(), false, 0, GraphQLType::float()], + [Type::string(), false, 0, GraphQLType::string()], + [Type::array(), false, 0, 'Iterable'], + [Type::iterable(), false, 0, 'Iterable'], + [Type::object(\DateTimeInterface::class), false, 0, GraphQLType::string()], + [Type::object(GenderTypeEnum::class), false, 0, new EnumType(['name' => 'GenderTypeEnum', 'values' => []])], + [Type::object(), false, 0, null], + [Type::callable(), false, 0, null], + [Type::null(), false, 0, null], + [Type::resource(), false, 0, null], ]; } - public function testConvertTypeNoGraphQlResourceMetadata(): void + #[IgnoreDeprecations] + public function testConvertTypeNoGraphQlResourceMetadataLegacy(): void { - $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy'); + $type = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'dummy'); $this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(false); $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('dummy', [new ApiResource()])); - /** @var Operation $operation */ $operation = (new Query())->withName('test'); $graphqlType = $this->typeConverter->convertType($type, false, $operation, 'resourceClass', 'rootClass', null, 0); $this->assertNull($graphqlType); } - public function testConvertTypeNodeResource(): void + public function testConvertTypeNoGraphQlResourceMetadata(): void { - $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'node'); + $type = Type::object('dummy'); + + $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('dummy', [new ApiResource()])); + + $operation = (new Query())->withName('test'); + $graphqlType = $this->typeConverter->convertPhpType($type, false, $operation, 'resourceClass', 'rootClass', null, 0); + $this->assertNull($graphqlType); + } + + #[IgnoreDeprecations] + public function testConvertTypeNodeResourceLegacy(): void + { + $type = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'node'); $this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(false); $this->resourceMetadataCollectionFactoryProphecy->create('node')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('node', [(new ApiResource())->withShortName('Node')->withGraphQlOperations(['test' => new Query()])])); @@ -117,45 +161,80 @@ public function testConvertTypeNodeResource(): void $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage('A "Node" resource cannot be used with GraphQL because the type is already used by the Relay specification.'); - /** @var Operation $operation */ $operation = (new Query())->withName('test'); $this->typeConverter->convertType($type, false, $operation, 'resourceClass', 'rootClass', null, 0); } - public function testConvertTypeResourceClassNotFound(): void + public function testConvertTypeNodeResource(): void { - $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy'); + $type = Type::object('node'); + + $this->resourceMetadataCollectionFactoryProphecy->create('node')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('node', [(new ApiResource())->withShortName('Node')->withGraphQlOperations(['test' => new Query()])])); + + $this->expectException(\UnexpectedValueException::class); + $this->expectExceptionMessage('A "Node" resource cannot be used with GraphQL because the type is already used by the Relay specification.'); + + $operation = (new Query())->withName('test'); + $this->typeConverter->convertPhpType($type, false, $operation, 'resourceClass', 'rootClass', null, 0); + } + + #[IgnoreDeprecations] + public function testConvertTypeResourceClassNotFoundLegacy(): void + { + $type = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'dummy'); $this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(false); $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->shouldBeCalled()->willThrow(new ResourceClassNotFoundException()); - /** @var Operation $operation */ $operation = (new Query())->withName('test'); $graphqlType = $this->typeConverter->convertType($type, false, $operation, 'resourceClass', 'rootClass', null, 0); $this->assertNull($graphqlType); } - public function testConvertTypeResourceIri(): void + public function testConvertTypeResourceClassNotFound(): void { - $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy'); + $type = Type::object('dummy'); + + $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->shouldBeCalled()->willThrow(new ResourceClassNotFoundException()); + + $operation = (new Query())->withName('test'); + $graphqlType = $this->typeConverter->convertPhpType($type, false, $operation, 'resourceClass', 'rootClass', null, 0); + $this->assertNull($graphqlType); + } + + #[IgnoreDeprecations] + public function testConvertTypeResourceIriLegacy(): void + { + $type = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'dummy'); $graphqlResourceMetadata = new ResourceMetadataCollection('dummy', [(new ApiResource())->withGraphQlOperations(['test' => new Query()])]); $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->willReturn($graphqlResourceMetadata); $this->typeBuilderProphecy->isCollection($type)->willReturn(false); $this->propertyMetadataFactoryProphecy->create('rootClass', 'dummyProperty', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withWritableLink(false)); - /** @var Operation $operation */ $operation = (new Query())->withName('test'); $graphqlType = $this->typeConverter->convertType($type, true, $operation, 'dummy', 'rootClass', 'dummyProperty', 1); $this->assertSame(GraphQLType::string(), $graphqlType); } - public function testConvertTypeInputResource(): void + public function testConvertTypeResourceIri(): void + { + $type = Type::object('dummy'); + + $graphqlResourceMetadata = new ResourceMetadataCollection('dummy', [(new ApiResource())->withGraphQlOperations(['test' => new Query()])]); + $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->willReturn($graphqlResourceMetadata); + $this->propertyMetadataFactoryProphecy->create('rootClass', 'dummyProperty', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withWritableLink(false)); + + $operation = (new Query())->withName('test'); + $graphqlType = $this->typeConverter->convertPhpType($type, true, $operation, 'dummy', 'rootClass', 'dummyProperty', 1); + $this->assertSame(GraphQLType::string(), $graphqlType); + } + + #[IgnoreDeprecations] + public function testConvertTypeInputResourceLegacy(): void { - $type = new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummy'); - /** @var Operation $operation */ + $type = new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'dummy'); $operation = new Query(); - /** @var ApiProperty $propertyMetadata */ $propertyMetadata = (new ApiProperty())->withWritableLink(true); $graphqlResourceMetadata = new ResourceMetadataCollection('dummy', [(new ApiResource())->withGraphQlOperations(['item_query' => $operation])]); $expectedGraphqlType = new ObjectType(['name' => 'resourceObjectType', 'fields' => []]); @@ -169,10 +248,25 @@ public function testConvertTypeInputResource(): void $this->assertSame($expectedGraphqlType, $graphqlType); } - /** - * @dataProvider convertTypeResourceProvider - */ - public function testConvertTypeCollectionResource(Type $type, ObjectType $expectedGraphqlType): void + public function testConvertTypeInputResource(): void + { + $type = Type::object('dummy'); + $operation = new Query(); + $propertyMetadata = (new ApiProperty())->withWritableLink(true); + $graphqlResourceMetadata = new ResourceMetadataCollection('dummy', [(new ApiResource())->withGraphQlOperations(['item_query' => $operation])]); + $expectedGraphqlType = new ObjectType(['name' => 'resourceObjectType', 'fields' => []]); + + $this->resourceMetadataCollectionFactoryProphecy->create('dummy')->willReturn($graphqlResourceMetadata); + $this->propertyMetadataFactoryProphecy->create('rootClass', 'dummyProperty', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withWritableLink(true)); + $this->typeBuilderProphecy->getResourceObjectType($graphqlResourceMetadata, $operation, $propertyMetadata, ['input' => true, 'wrapped' => false, 'depth' => 1])->shouldBeCalled()->willReturn($expectedGraphqlType); + + $graphqlType = $this->typeConverter->convertPhpType($type, true, $operation, 'dummy', 'rootClass', 'dummyProperty', 1); + $this->assertSame($expectedGraphqlType, $graphqlType); + } + + #[DataProvider('legacyConvertTypeResourceProvider')] + #[IgnoreDeprecations] + public function testConvertTypeCollectionResourceLegacy(LegacyType $type, ObjectType $expectedGraphqlType): void { $collectionOperation = new QueryCollection(); $graphqlResourceMetadata = new ResourceMetadataCollection('dummyValue', [ @@ -187,37 +281,74 @@ public function testConvertTypeCollectionResource(Type $type, ObjectType $expect 'depth' => 0, ])->shouldBeCalled()->willReturn($expectedGraphqlType); - /** @var Operation $rootOperation */ $rootOperation = (new Query())->withName('test'); $graphqlType = $this->typeConverter->convertType($type, false, $rootOperation, 'resourceClass', 'rootClass', null, 0); $this->assertSame($expectedGraphqlType, $graphqlType); } + public static function legacyConvertTypeResourceProvider(): array + { + return [ + [new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, null, true, null, new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'dummyValue')), new ObjectType(['name' => 'resourceObjectType', 'fields' => []])], + [new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, null, new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, 'dummyValue')), new ObjectType(['name' => 'resourceObjectType', 'fields' => []])], + ]; + } + + #[DataProvider('convertTypeResourceProvider')] + public function testConvertTypeCollectionResource(Type $type, ObjectType $expectedGraphqlType): void + { + $collectionOperation = new QueryCollection(); + $graphqlResourceMetadata = new ResourceMetadataCollection('dummyValue', [ + (new ApiResource())->withShortName('DummyValue')->withGraphQlOperations(['collection_query' => $collectionOperation]), + ]); + + $this->resourceMetadataCollectionFactoryProphecy->create('dummyValue')->shouldBeCalled()->willReturn($graphqlResourceMetadata); + $this->typeBuilderProphecy->getResourceObjectType($graphqlResourceMetadata, $collectionOperation, null, [ + 'input' => false, + 'wrapped' => false, + 'depth' => 0, + ])->shouldBeCalled()->willReturn($expectedGraphqlType); + + $rootOperation = (new Query())->withName('test'); + $graphqlType = $this->typeConverter->convertPhpType($type, false, $rootOperation, 'resourceClass', 'rootClass', null, 0); + $this->assertSame($expectedGraphqlType, $graphqlType); + } + public static function convertTypeResourceProvider(): array { return [ - [new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummyValue')), new ObjectType(['name' => 'resourceObjectType', 'fields' => []])], - [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, 'dummyValue')), new ObjectType(['name' => 'resourceObjectType', 'fields' => []])], + [Type::collection(Type::object('dummyValue'), Type::object('dummyValue')), new ObjectType(['name' => 'resourceObjectType', 'fields' => []])], // @phpstan-ignore-line + [Type::array(Type::object('dummyValue')), new ObjectType(['name' => 'resourceObjectType', 'fields' => []])], ]; } - public function testConvertTypeCollectionEnum(): void + #[IgnoreDeprecations] + public function testConvertTypeCollectionEnumLegacy(): void { - $type = new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)); + $type = new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, null, new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)); $expectedGraphqlType = new EnumType(['name' => 'GenderTypeEnum', 'values' => []]); $this->typeBuilderProphecy->isCollection($type)->shouldBeCalled()->willReturn(true); $this->resourceMetadataCollectionFactoryProphecy->create(GenderTypeEnum::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(GenderTypeEnum::class, [])); $this->typeBuilderProphecy->getEnumType(Argument::type(Operation::class))->willReturn($expectedGraphqlType); - /** @var Operation $rootOperation */ $rootOperation = (new Query())->withName('test'); $graphqlType = $this->typeConverter->convertType($type, false, $rootOperation, 'resourceClass', 'rootClass', null, 0); $this->assertSame($expectedGraphqlType, $graphqlType); } - /** - * @dataProvider resolveTypeProvider - */ + public function testConvertTypeCollectionEnum(): void + { + $type = Type::array(Type::object(GenderTypeEnum::class)); + $expectedGraphqlType = new EnumType(['name' => 'GenderTypeEnum', 'values' => []]); + $this->resourceMetadataCollectionFactoryProphecy->create(GenderTypeEnum::class)->shouldBeCalled()->willReturn(new ResourceMetadataCollection(GenderTypeEnum::class, [])); + $this->typeBuilderProphecy->getEnumType(Argument::type(Operation::class))->willReturn($expectedGraphqlType); + + $rootOperation = (new Query())->withName('test'); + $graphqlType = $this->typeConverter->convertPhpType($type, false, $rootOperation, 'resourceClass', 'rootClass', null, 0); + $this->assertSame($expectedGraphqlType, $graphqlType); + } + + #[DataProvider('resolveTypeProvider')] public function testResolveType(string $type, string|GraphQLType $expectedGraphqlType): void { $this->typesContainerProphecy->has(\DateTime::class)->willReturn(true); @@ -242,9 +373,7 @@ public static function resolveTypeProvider(): array ]; } - /** - * @dataProvider resolveTypeInvalidProvider - */ + #[DataProvider('resolveTypeInvalidProvider')] public function testResolveTypeInvalid(string $type, string $expectedExceptionMessage): void { $this->typesContainerProphecy->has('UnknownType')->willReturn(false); diff --git a/src/GraphQl/Type/ContextAwareTypeBuilderInterface.php b/src/GraphQl/Type/ContextAwareTypeBuilderInterface.php index 6f96189a4c6..d945ff175e5 100644 --- a/src/GraphQl/Type/ContextAwareTypeBuilderInterface.php +++ b/src/GraphQl/Type/ContextAwareTypeBuilderInterface.php @@ -18,7 +18,8 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\Type as GraphQLType; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * Interface implemented to build a GraphQL type. @@ -30,7 +31,11 @@ interface ContextAwareTypeBuilderInterface /** * Gets the object type of the given resource. * - * @param array&array{input?: bool, wrapped?: bool, depth?: int} $context + * @param array $context + * + * @phpstan-param array $context + * + * @psalm-param array{input?: bool, wrapped?: bool, depth?: int, ...} $context * * @return GraphQLType the object type, possibly wrapped by NonNull */ @@ -53,6 +58,8 @@ public function getEnumType(Operation $operation): GraphQLType; /** * Returns true if a type is a collection. + * + * @deprecated since 4.2 */ - public function isCollection(Type $type): bool; + public function isCollection(LegacyType $type): bool; } diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 457a0772b0a..c19b47113bf 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -13,20 +13,23 @@ namespace ApiPlatform\GraphQl\Type; -use ApiPlatform\Doctrine\Odm\State\Options as ODMOptions; -use ApiPlatform\Doctrine\Orm\State\Options; -use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory; +use ApiPlatform\GraphQl\Exception\InvalidTypeException; use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; use ApiPlatform\GraphQl\Type\Definition\TypeInterface; +use ApiPlatform\Metadata\FilterInterface; use ApiPlatform\Metadata\GraphQl\Mutation; use ApiPlatform\Metadata\GraphQl\Operation; use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\GraphQl\Subscription; +use ApiPlatform\Metadata\InflectorInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\Util\Inflector; +use ApiPlatform\Metadata\Util\PropertyInfoToTypeInfoHelper; +use ApiPlatform\Metadata\Util\TypeHelper; use ApiPlatform\State\Pagination\Pagination; use GraphQL\Type\Definition\InputObjectType; use GraphQL\Type\Definition\ListOfType; @@ -35,29 +38,27 @@ use GraphQL\Type\Definition\Type as GraphQLType; use GraphQL\Type\Definition\WrappingType; use Psr\Container\ContainerInterface; -use Symfony\Component\Config\Definition\Exception\InvalidTypeException; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Builds the GraphQL fields. * * @author Alan Poulain */ -final class FieldsBuilder implements FieldsBuilderInterface, FieldsBuilderEnumInterface +final class FieldsBuilder implements FieldsBuilderEnumInterface { - private readonly ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder; + private readonly ContextAwareTypeBuilderInterface $typeBuilder; - public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $itemResolverFactory, private readonly ?ResolverFactoryInterface $collectionResolverFactory, private readonly ?ResolverFactoryInterface $itemMutationResolverFactory, private readonly ?ResolverFactoryInterface $itemSubscriptionResolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator) + public function __construct(private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly TypesContainerInterface $typesContainer, ContextAwareTypeBuilderInterface $typeBuilder, private readonly TypeConverterInterface $typeConverter, private readonly ResolverFactoryInterface $resolverFactory, private readonly ContainerInterface $filterLocator, private readonly Pagination $pagination, private readonly ?NameConverterInterface $nameConverter, private readonly string $nestingSeparator, private readonly ?InflectorInterface $inflector = new Inflector()) { - if ($typeBuilder instanceof TypeBuilderInterface) { - @trigger_error(\sprintf('$typeBuilder argument of FieldsBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED); - } - if ($typeBuilder instanceof TypeBuilderEnumInterface) { - @trigger_error(\sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.3. It has to implement "%s" instead.', TypeBuilderEnumInterface::class, ContextAwareTypeBuilderInterface::class), \E_USER_DEPRECATED); - } $this->typeBuilder = $typeBuilder; } @@ -71,7 +72,7 @@ public function getNodeQueryFields(): array 'args' => [ 'id' => ['type' => GraphQLType::nonNull(GraphQLType::id())], ], - 'resolve' => ($this->itemResolverFactory)(), + 'resolve' => ($this->resolverFactory)(), ]; } @@ -86,7 +87,7 @@ public function getItemQueryFields(string $resourceClass, Operation $operation, $fieldName = lcfirst('item_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName()); - if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass), $resourceClass, false, $operation)) { + if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), Type::nullable(Type::object($resourceClass)), $resourceClass, false, $operation)) { $args = $this->resolveResourceArgs($configuration['args'] ?? [], $operation); $extraArgs = $this->resolveResourceArgs($operation->getExtraArgs() ?? [], $operation); $configuration['args'] = $args ?: $configuration['args'] ?? ['id' => ['type' => GraphQLType::nonNull(GraphQLType::id())]] + $extraArgs; @@ -108,12 +109,12 @@ public function getCollectionQueryFields(string $resourceClass, Operation $opera $fieldName = lcfirst('collection_query' === $operation->getName() ? $operation->getShortName() : $operation->getName().$operation->getShortName()); - if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), new Type(Type::BUILTIN_TYPE_OBJECT, false, null, true, null, new Type(Type::BUILTIN_TYPE_OBJECT, false, $resourceClass)), $resourceClass, false, $operation)) { + if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $operation->getDescription(), $operation->getDeprecationReason(), Type::collection(Type::object(\stdClass::class), Type::object($resourceClass)), $resourceClass, false, $operation)) { $args = $this->resolveResourceArgs($configuration['args'] ?? [], $operation); $extraArgs = $this->resolveResourceArgs($operation->getExtraArgs() ?? [], $operation); $configuration['args'] = $args ?: $configuration['args'] ?? $fieldConfiguration['args'] + $extraArgs; - return [Inflector::pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)]; + return [$this->inflector->pluralize($fieldName) => array_merge($fieldConfiguration, $configuration)]; } return []; @@ -125,7 +126,7 @@ public function getCollectionQueryFields(string $resourceClass, Operation $opera public function getMutationFields(string $resourceClass, Operation $operation): array { $mutationFields = []; - $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass); + $resourceType = Type::nullable(Type::object($resourceClass)); $description = $operation->getDescription() ?? ucfirst("{$operation->getName()}s a {$operation->getShortName()}."); if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) { @@ -143,7 +144,7 @@ public function getMutationFields(string $resourceClass, Operation $operation): public function getSubscriptionFields(string $resourceClass, Operation $operation): array { $subscriptionFields = []; - $resourceType = new Type(Type::BUILTIN_TYPE_OBJECT, true, $resourceClass); + $resourceType = Type::nullable(Type::object($resourceClass)); $description = $operation->getDescription() ?? \sprintf('Subscribes to the action event of a %s.', $operation->getShortName()); if ($fieldConfiguration = $this->getResourceFieldConfiguration(null, $description, $operation->getDeprecationReason(), $resourceType, $resourceClass, false, $operation)) { @@ -219,22 +220,37 @@ public function getResourceObjectTypeFields(?string $resourceClass, Operation $o 'denormalization_groups' => $operation->getDenormalizationContext()['groups'] ?? null, ]; $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $context); - $propertyTypes = $propertyMetadata->getBuiltinTypes(); - - if ( - !$propertyTypes - || (!$input && false === $propertyMetadata->isReadable()) - || ($input && false === $propertyMetadata->isWritable()) - ) { - continue; - } - // guess union/intersect types: check each type until finding a valid one - foreach ($propertyTypes as $propertyType) { + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyTypes = $propertyMetadata->getBuiltinTypes(); + + if ( + !$propertyTypes + || (!$input && false === $propertyMetadata->isReadable()) + || ($input && false === $propertyMetadata->isWritable()) + ) { + continue; + } + + // guess union/intersect types: check each type until finding a valid one + foreach ($propertyTypes as $propertyType) { + if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getDeprecationReason(), $propertyType, $resourceClass, $input, $operation, $depth, null !== $propertyMetadata->getSecurity())) { + $fields['id' === $property ? '_id' : $this->normalizePropertyName($property, $resourceClass)] = $fieldConfiguration; + // stop at the first valid type + break; + } + } + } else { + if ( + !($propertyType = $propertyMetadata->getNativeType()) + || (!$input && false === $propertyMetadata->isReadable()) + || ($input && false === $propertyMetadata->isWritable()) + ) { + continue; + } + if ($fieldConfiguration = $this->getResourceFieldConfiguration($property, $propertyMetadata->getDescription(), $propertyMetadata->getDeprecationReason(), $propertyType, $resourceClass, $input, $operation, $depth, null !== $propertyMetadata->getSecurity())) { $fields['id' === $property ? '_id' : $this->normalizePropertyName($property, $resourceClass)] = $fieldConfiguration; - // stop at the first valid type - break; } } } @@ -291,41 +307,6 @@ public function resolveResourceArgs(array $args, Operation $operation): array $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']); } - /* - * This is @experimental, read the comment on the parameterToObjectType function as additional information. - */ - foreach ($operation->getParameters() ?? [] as $parameter) { - $key = $parameter->getKey(); - - if (str_contains($key, ':property')) { - if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) { - continue; - } - - $parsedKey = explode('[:property]', $key); - $flattenFields = []; - foreach ($this->filterLocator->get($filterId)->getDescription($operation->getClass()) as $key => $value) { - $values = []; - parse_str($key, $values); - if (isset($values[$parsedKey[0]])) { - $values = $values[$parsedKey[0]]; - } - - $name = key($values); - $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string']; - } - - $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]); - continue; - } - - $args[$key] = ['type' => GraphQLType::string()]; - - if ($parameter->getRequired()) { - $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']); - } - } - return $args; } @@ -342,8 +323,12 @@ private function parameterToObjectType(array $flattenFields, string $name): Inpu $fields = []; foreach ($flattenFields as $field) { $key = $field['name']; - $type = $this->getParameterType(\in_array($field['type'], Type::$builtinTypes, true) ? new Type($field['type'], !$field['required']) : new Type('object', !$field['required'], $field['type'])); + $type = \in_array($field['type'], TypeIdentifier::values(), true) ? Type::builtin($field['type']) : Type::object($field['type']); + if (!$field['required']) { + $type = Type::nullable($type); + } + $type = $this->getParameterType($type); if (\is_array($l = $field['leafs'])) { if (0 === key($l)) { $key = $key; @@ -384,16 +369,27 @@ private function parameterToObjectType(array $flattenFields, string $name): Inpu */ private function getParameterType(Type $type): GraphQLType { - return match ($type->getBuiltinType()) { - Type::BUILTIN_TYPE_BOOL => GraphQLType::boolean(), - Type::BUILTIN_TYPE_INT => GraphQLType::int(), - Type::BUILTIN_TYPE_FLOAT => GraphQLType::float(), - Type::BUILTIN_TYPE_STRING => GraphQLType::string(), - Type::BUILTIN_TYPE_ARRAY => GraphQLType::listOf($this->getParameterType($type->getCollectionValueTypes()[0])), - Type::BUILTIN_TYPE_ITERABLE => GraphQLType::listOf($this->getParameterType($type->getCollectionValueTypes()[0])), - Type::BUILTIN_TYPE_OBJECT => GraphQLType::string(), - default => GraphQLType::string(), - }; + if ($type->isIdentifiedBy(TypeIdentifier::BOOL)) { + return GraphQLType::boolean(); + } + + if ($type->isIdentifiedBy(TypeIdentifier::INT)) { + return GraphQLType::int(); + } + + if ($type->isIdentifiedBy(TypeIdentifier::FLOAT)) { + return GraphQLType::float(); + } + + if ($type->isIdentifiedBy(TypeIdentifier::STRING, TypeIdentifier::OBJECT)) { + return GraphQLType::string(); + } + + if ($type instanceof CollectionType) { + return GraphQLType::listOf($this->getParameterType($type->getCollectionValueType())); + } + + return GraphQLType::string(); } /** @@ -401,22 +397,30 @@ private function getParameterType(Type $type): GraphQLType * * @see http://webonyx.github.io/graphql-php/type-system/object-types/ */ - private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, ?string $deprecationReason, Type $type, string $rootResource, bool $input, Operation $rootOperation, int $depth = 0, bool $forceNullable = false): ?array + private function getResourceFieldConfiguration(?string $property, ?string $fieldDescription, ?string $deprecationReason, Type|LegacyType $type, string $rootResource, bool $input, Operation $rootOperation, int $depth = 0, bool $forceNullable = false): ?array { + if ($type instanceof LegacyType) { + $type = PropertyInfoToTypeInfoHelper::convertLegacyTypesToType([$type]); + } + try { - $isCollectionType = $this->typeBuilder->isCollection($type); + $isCollectionType = $type->isSatisfiedBy(fn ($t) => $t instanceof CollectionType) && ($v = TypeHelper::getCollectionValueType($type)) && TypeHelper::getClassName($v); - if ( - $isCollectionType - && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null - ) { - $resourceClass = $collectionValueType->getClassName(); - } else { - $resourceClass = $type->getClassName(); + $valueType = $type; + if ($isCollectionType) { + $valueType = TypeHelper::getCollectionValueType($type); } + /** @var class-string|null $resourceClass */ + $resourceClass = null; + $typeIsResourceClass = function (Type $type) use (&$resourceClass): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($resourceClass = $type->getClassName()); + }; + + $isResourceClass = $valueType->isSatisfiedBy($typeIsResourceClass); + $resourceOperation = $rootOperation; - if ($resourceClass && $depth >= 1 && $this->resourceClassResolver->isResourceClass($resourceClass)) { + if ($resourceClass && $depth >= 1 && $isResourceClass) { $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); $resourceOperation = $resourceMetadataCollection->getOperation($isCollectionType ? 'collection_query' : 'item_query'); } @@ -447,30 +451,29 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $args = []; - if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) { - if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) { - $args = $this->getGraphQlPaginationArgs($resourceOperation); - } + if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType) { + if ($isCollectionType) { + if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) { + $args = $this->getGraphQlPaginationArgs($resourceOperation); + } - $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); - } + $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); - if ($this->itemResolverFactory instanceof ResolverFactory) { - if ($isStandardGraphqlType || $input) { - $resolve = null; - } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory); + // Also register parameter args in the types container + // Note: This is a workaround, for more information read the comment on the parameterToObjectType function. + foreach ($this->getParameterArgs($rootOperation) as $key => $arg) { + if ($arg instanceof InputObjectType || (\is_array($arg) && isset($arg['name']))) { + $this->typesContainer->set(\is_array($arg) ? $arg['name'] : $arg->name(), $arg); + } + $args[$key] = $arg; + } } + } + + if ($isStandardGraphqlType || $input) { + $resolve = null; } else { - if ($isStandardGraphqlType || $input) { - $resolve = null; - } elseif (($rootOperation instanceof Mutation || $rootOperation instanceof Subscription) && $depth <= 0) { - $resolve = $rootOperation instanceof Mutation ? ($this->itemMutationResolverFactory)($resourceClass, $rootResource, $resourceOperation) : ($this->itemSubscriptionResolverFactory)($resourceClass, $rootResource, $resourceOperation); - } elseif ($this->typeBuilder->isCollection($type)) { - $resolve = ($this->collectionResolverFactory)($resourceClass, $rootResource, $resourceOperation); - } else { - $resolve = ($this->itemResolverFactory)($resourceClass, $rootResource, $resourceOperation); - } + $resolve = ($this->resolverFactory)($resourceClass, $rootResource, $resourceOperation, $this->propertyMetadataFactory); } return [ @@ -487,6 +490,67 @@ private function getResourceFieldConfiguration(?string $property, ?string $field return null; } + /* + * This function is @experimental, read the comment on the parameterToObjectType function for additional information. + * @experimental + */ + private function getParameterArgs(Operation $operation, array $args = []): array + { + foreach ($operation->getParameters() ?? [] as $parameter) { + $key = $parameter->getKey(); + + if (!str_contains($key, ':property')) { + $args[$key] = ['type' => GraphQLType::string()]; + + if ($parameter->getRequired()) { + $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']); + } + + continue; + } + + if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) { + continue; + } + + $filter = $this->filterLocator->get($filterId); + $parsedKey = explode('[:property]', $key); + $flattenFields = []; + + if ($filter instanceof FilterInterface) { + foreach ($filter->getDescription($operation->getClass()) as $name => $value) { + $values = []; + parse_str($name, $values); + if (isset($values[$parsedKey[0]])) { + $values = $values[$parsedKey[0]]; + } + + $name = key($values); + $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string']; + } + + $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]); + } + + if ($filter instanceof OpenApiParameterFilterInterface) { + foreach ($filter->getOpenApiParameters($parameter) as $value) { + $values = []; + parse_str($value->getName(), $values); + if (isset($values[$parsedKey[0]])) { + $values = $values[$parsedKey[0]]; + } + + $name = key($values); + $flattenFields[] = ['name' => $name, 'required' => $value->getRequired(), 'description' => $value->getDescription(), 'leafs' => $values[$name], 'type' => $value->getSchema()['type'] ?? 'string']; + } + + $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0].$operation->getShortName().$operation->getName()); + } + } + + return $args; + } + private function getGraphQlPaginationArgs(Operation $queryOperation): array { $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation); @@ -542,20 +606,11 @@ private function getFilterArgs(array $args, ?string $resourceClass, string $root continue; } - $entityClass = $resourceClass; - if ($options = $resourceOperation->getStateOptions()) { - if ($options instanceof Options && $options->getEntityClass()) { - $entityClass = $options->getEntityClass(); - } - - if ($options instanceof ODMOptions && $options->getDocumentClass()) { - $entityClass = $options->getDocumentClass(); + foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $description) { + $filterType = \in_array($description['type'], TypeIdentifier::values(), true) ? Type::builtin($description['type']) : Type::object($description['type']); + if (!($description['required'] ?? false)) { + $filterType = Type::nullable($filterType); } - } - - foreach ($this->filterLocator->get($filterId)->getDescription($entityClass) as $key => $description) { - $nullable = isset($description['required']) ? !$description['required'] : true; - $filterType = \in_array($description['type'], Type::$builtinTypes, true) ? new Type($description['type'], $nullable) : new Type('object', $nullable, $description['type']); $graphqlFilterType = $this->convertType($filterType, false, $resourceOperation, $rootOperation, $resourceClass, $rootResource, $property, $depth); if (str_ends_with($key, '[]')) { @@ -644,12 +699,16 @@ private function convertFilterArgsToTypes(array $args): array * * @throws InvalidTypeException */ - private function convertType(Type $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull + private function convertType(Type|LegacyType $type, bool $input, Operation $resourceOperation, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth, bool $forceNullable = false): GraphQLType|ListOfType|NonNull { - $graphqlType = $this->typeConverter->convertType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); + if ($type instanceof LegacyType) { + $type = PropertyInfoToTypeInfoHelper::convertLegacyTypesToType([$type]); + } + + $graphqlType = $this->typeConverter->convertPhpType($type, $input, $rootOperation, $resourceClass, $rootResource, $property, $depth); if (null === $graphqlType) { - throw new InvalidTypeException(\sprintf('The type "%s" is not supported.', $type->getBuiltinType())); + throw new InvalidTypeException(\sprintf('The type "%s" is not supported.', (string) $type)); } if (\is_string($graphqlType)) { @@ -660,13 +719,8 @@ private function convertType(Type $type, bool $input, Operation $resourceOperati $graphqlType = $this->typesContainer->get($graphqlType); } - if ($this->typeBuilder->isCollection($type)) { + if ($type->isSatisfiedBy(fn ($t) => $t instanceof CollectionType) && ($collectionValueType = TypeHelper::getCollectionValueType($type)) && TypeHelper::getClassName($collectionValueType)) { if (!$input && !$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) { - // Deprecated path, to remove in API Platform 4. - if ($this->typeBuilder instanceof TypeBuilderInterface) { - return $this->typeBuilder->getResourcePaginatedCollectionType($graphqlType, $resourceClass, $resourceOperation); - } - return $this->typeBuilder->getPaginatedCollectionType($graphqlType, $resourceOperation); } diff --git a/src/GraphQl/Type/FieldsBuilderInterface.php b/src/GraphQl/Type/FieldsBuilderInterface.php deleted file mode 100644 index dc4bd57f003..00000000000 --- a/src/GraphQl/Type/FieldsBuilderInterface.php +++ /dev/null @@ -1,61 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Type; - -use ApiPlatform\Metadata\GraphQl\Operation; - -/** - * Interface implemented to build GraphQL fields. - * - * @author Alan Poulain - * - * @deprecated Since API Platform 3.1. Use @see FieldsBuilderEnumInterface instead. - */ -interface FieldsBuilderInterface -{ - /** - * Gets the fields of a node for a query. - */ - public function getNodeQueryFields(): array; - - /** - * Gets the item query fields of the schema. - */ - public function getItemQueryFields(string $resourceClass, Operation $operation, array $configuration): array; - - /** - * Gets the collection query fields of the schema. - */ - public function getCollectionQueryFields(string $resourceClass, Operation $operation, array $configuration): array; - - /** - * Gets the mutation fields of the schema. - */ - public function getMutationFields(string $resourceClass, Operation $operation): array; - - /** - * Gets the subscription fields of the schema. - */ - public function getSubscriptionFields(string $resourceClass, Operation $operation): array; - - /** - * Gets the fields of the type of the given resource. - */ - public function getResourceObjectTypeFields(?string $resourceClass, Operation $operation, bool $input, int $depth = 0, ?array $ioMetadata = null): array; - - /** - * Resolve the args of a resource by resolving its types. - */ - public function resolveResourceArgs(array $args, Operation $operation): array; -} diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index dd91a2e3c9e..544ec35acbf 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -32,11 +32,8 @@ */ final class SchemaBuilder implements SchemaBuilderInterface { - public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly TypesFactoryInterface $typesFactory, private readonly TypesContainerInterface $typesContainer, private readonly FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder) + public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly TypesFactoryInterface $typesFactory, private readonly TypesContainerInterface $typesContainer, private readonly FieldsBuilderEnumInterface $fieldsBuilder) { - if ($this->fieldsBuilder instanceof FieldsBuilderInterface) { - @trigger_error(\sprintf('$fieldsBuilder argument of SchemaBuilder implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED); - } } public function getSchema(): Schema diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index b900e97622f..43152f0b6dd 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -30,7 +30,8 @@ use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type as GraphQLType; use Psr\Container\ContainerInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * Builds the GraphQL types. @@ -41,11 +42,17 @@ final class TypeBuilder implements ContextAwareTypeBuilderInterface { private $defaultFieldResolver; - public function __construct(private readonly TypesContainerInterface $typesContainer, callable $defaultFieldResolver, private readonly ContainerInterface $fieldsBuilderLocator, private readonly Pagination $pagination) + public function __construct(private readonly TypesContainerInterface $typesContainer, callable $defaultFieldResolver, private ?ContainerInterface $fieldsBuilderLocator, private readonly Pagination $pagination) { + $this->fieldsBuilderLocator = $fieldsBuilderLocator; $this->defaultFieldResolver = $defaultFieldResolver; } + public function setFieldsBuilderLocator(ContainerInterface $fieldsBuilderLocator): void + { + $this->fieldsBuilderLocator = $fieldsBuilderLocator; + } + /** * {@inheritdoc} */ @@ -195,15 +202,10 @@ public function getEnumType(Operation $operation): GraphQLType return $this->typesContainer->get($enumName); } - /** @var FieldsBuilderEnumInterface|FieldsBuilderInterface $fieldsBuilder */ + /** @var FieldsBuilderEnumInterface $fieldsBuilder */ $fieldsBuilder = $this->fieldsBuilderLocator->get('api_platform.graphql.fields_builder'); $enumCases = []; - // Remove the condition in API Platform 4. - if ($fieldsBuilder instanceof FieldsBuilderEnumInterface) { - $enumCases = $fieldsBuilder->getEnumFields($operation->getClass()); - } else { - @trigger_error(\sprintf('api_platform.graphql.fields_builder service implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', FieldsBuilderInterface::class, FieldsBuilderEnumInterface::class), \E_USER_DEPRECATED); - } + $enumCases = $fieldsBuilder->getEnumFields($operation->getClass()); $enumConfig = [ 'name' => $enumName, @@ -222,8 +224,10 @@ public function getEnumType(Operation $operation): GraphQLType /** * {@inheritdoc} */ - public function isCollection(Type $type): bool + public function isCollection(LegacyType $type): bool { + trigger_deprecation('api-platform/graphql', '4.2', 'The "%s()" method is deprecated and will be removed.', __METHOD__, self::class); + return $type->isCollection() && ($collectionValueType = $type->getCollectionValueTypes()[0] ?? null) && null !== $collectionValueType->getClassName(); } @@ -277,6 +281,7 @@ private function getPageBasedPaginationFields(GraphQLType $resourceType): array 'itemsPerPage' => GraphQLType::nonNull(GraphQLType::int()), 'lastPage' => GraphQLType::nonNull(GraphQLType::int()), 'totalCount' => GraphQLType::nonNull(GraphQLType::int()), + 'currentPage' => GraphQLType::nonNull(GraphQLType::int()), 'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()), ], ]; diff --git a/src/GraphQl/Type/TypeBuilderEnumInterface.php b/src/GraphQl/Type/TypeBuilderEnumInterface.php deleted file mode 100644 index 6e674f452d7..00000000000 --- a/src/GraphQl/Type/TypeBuilderEnumInterface.php +++ /dev/null @@ -1,58 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Type; - -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use GraphQL\Type\Definition\InterfaceType; -use GraphQL\Type\Definition\Type as GraphQLType; -use Symfony\Component\PropertyInfo\Type; - -/** - * Interface implemented to build a GraphQL type. - * - * @author Alan Poulain - * - * @deprecated Since API Platform 3.3. Use @see ContextAwareTypeBuilderInterface instead. - */ -interface TypeBuilderEnumInterface -{ - /** - * Gets the object type of the given resource. - * - * @return GraphQLType the object type, possibly wrapped by NonNull - */ - public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0, ?ApiProperty $propertyMetadata = null): GraphQLType; - - /** - * Get the interface type of a node. - */ - public function getNodeInterface(): InterfaceType; - - /** - * Gets the type of a paginated collection of the given resource type. - */ - public function getPaginatedCollectionType(GraphQLType $resourceType, Operation $operation): GraphQLType; - - /** - * Gets the type corresponding to an enum. - */ - public function getEnumType(Operation $operation): GraphQLType; - - /** - * Returns true if a type is a collection. - */ - public function isCollection(Type $type): bool; -} diff --git a/src/GraphQl/Type/TypeBuilderInterface.php b/src/GraphQl/Type/TypeBuilderInterface.php deleted file mode 100644 index 8b782e32461..00000000000 --- a/src/GraphQl/Type/TypeBuilderInterface.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\GraphQl\Type; - -use ApiPlatform\Metadata\GraphQl\Operation; -use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use GraphQL\Type\Definition\InterfaceType; -use GraphQL\Type\Definition\NonNull; -use GraphQL\Type\Definition\ObjectType; -use GraphQL\Type\Definition\Type as GraphQLType; -use Symfony\Component\PropertyInfo\Type; - -/** - * Interface implemented to build a GraphQL type. - * - * @author Alan Poulain - * - * @deprecated Since API Platform 3.1. Use @see TypeBuilderEnumInterface instead. - */ -interface TypeBuilderInterface -{ - /** - * Gets the object type of the given resource. - * - * @return ObjectType|NonNull the object type, possibly wrapped by NonNull - */ - public function getResourceObjectType(?string $resourceClass, ResourceMetadataCollection $resourceMetadataCollection, Operation $operation, bool $input, bool $wrapped = false, int $depth = 0): GraphQLType; - - /** - * Get the interface type of a node. - */ - public function getNodeInterface(): InterfaceType; - - /** - * Gets the type of a paginated collection of the given resource type. - * - * @deprecated Since API Platform 3.1. Use @see TypeBuilderEnumInterface::getPaginatedCollectionType() method instead. - */ - public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, Operation $operation): GraphQLType; - - /** - * Returns true if a type is a collection. - */ - public function isCollection(Type $type): bool; -} diff --git a/src/GraphQl/Type/TypeConverter.php b/src/GraphQl/Type/TypeConverter.php index 69baa00b209..037a32f1209 100644 --- a/src/GraphQl/Type/TypeConverter.php +++ b/src/GraphQl/Type/TypeConverter.php @@ -20,6 +20,7 @@ use ApiPlatform\Metadata\GraphQl\Query; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Util\TypeHelper; use GraphQL\Error\SyntaxError; use GraphQL\Language\AST\ListTypeNode; use GraphQL\Language\AST\NamedTypeNode; @@ -28,7 +29,11 @@ use GraphQL\Language\Parser; use GraphQL\Type\Definition\NullableType; use GraphQL\Type\Definition\Type as GraphQLType; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * Converts a type to its GraphQL equivalent. @@ -37,39 +42,34 @@ */ final class TypeConverter implements TypeConverterInterface { - public function __construct(private readonly ContextAwareTypeBuilderInterface|TypeBuilderEnumInterface|TypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory) + public function __construct(private readonly ContextAwareTypeBuilderInterface $typeBuilder, private readonly TypesContainerInterface $typesContainer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory) { - if ($typeBuilder instanceof TypeBuilderInterface) { - @trigger_error(\sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.1. It has to implement "%s" instead.', TypeBuilderInterface::class, TypeBuilderEnumInterface::class), \E_USER_DEPRECATED); - } - - if ($typeBuilder instanceof TypeBuilderEnumInterface) { - @trigger_error(\sprintf('$typeBuilder argument of TypeConverter implementing "%s" is deprecated since API Platform 3.3. It has to implement "%s" instead.', TypeBuilderEnumInterface::class, ContextAwareTypeBuilderInterface::class), \E_USER_DEPRECATED); - } } /** * {@inheritdoc} */ - public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth): GraphQLType|string|null + public function convertType(LegacyType $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth): GraphQLType|string|null { + trigger_deprecation('api-platform/graphql', '4.2', 'The "%s()" method is deprecated, use "%s::convertPhpType()" instead.', __METHOD__, self::class); + switch ($type->getBuiltinType()) { - case Type::BUILTIN_TYPE_BOOL: + case LegacyType::BUILTIN_TYPE_BOOL: return GraphQLType::boolean(); - case Type::BUILTIN_TYPE_INT: + case LegacyType::BUILTIN_TYPE_INT: return GraphQLType::int(); - case Type::BUILTIN_TYPE_FLOAT: + case LegacyType::BUILTIN_TYPE_FLOAT: return GraphQLType::float(); - case Type::BUILTIN_TYPE_STRING: + case LegacyType::BUILTIN_TYPE_STRING: return GraphQLType::string(); - case Type::BUILTIN_TYPE_ARRAY: - case Type::BUILTIN_TYPE_ITERABLE: + case LegacyType::BUILTIN_TYPE_ARRAY: + case LegacyType::BUILTIN_TYPE_ITERABLE: if ($resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth)) { return $resourceType; } return 'Iterable'; - case Type::BUILTIN_TYPE_OBJECT: + case LegacyType::BUILTIN_TYPE_OBJECT: if (is_a($type->getClassName(), \DateTimeInterface::class, true)) { return GraphQLType::string(); } @@ -83,7 +83,43 @@ public function convertType(Type $type, bool $input, Operation $rootOperation, s /** * {@inheritdoc} */ - public function resolveType(string $type): ?GraphQLType + public function convertPhpType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth): GraphQLType|string|null + { + if ($type->isIdentifiedBy(TypeIdentifier::BOOL)) { + return GraphQLType::boolean(); + } + + if ($type->isIdentifiedBy(TypeIdentifier::INT)) { + return GraphQLType::int(); + } + + if ($type->isIdentifiedBy(TypeIdentifier::FLOAT)) { + return GraphQLType::float(); + } + + if ($type->isIdentifiedBy(TypeIdentifier::STRING, \DateTimeInterface::class)) { + return GraphQLType::string(); + } + + if ($type->isIdentifiedBy(TypeIdentifier::ARRAY, TypeIdentifier::ITERABLE)) { + if ($resourceType = $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth)) { + return $resourceType; + } + + return 'Iterable'; + } + + if ($type->isIdentifiedBy(TypeIdentifier::OBJECT)) { + return $this->getResourceType($type, $input, $rootOperation, $rootResource, $property, $depth); + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function resolveType(string $type): GraphQLType { try { $astTypeNode = Parser::parseType($type); @@ -98,19 +134,35 @@ public function resolveType(string $type): ?GraphQLType throw new InvalidArgumentException(\sprintf('The type "%s" was not resolved.', $type)); } - private function getResourceType(Type $type, bool $input, Operation $rootOperation, string $rootResource, ?string $property, int $depth): ?GraphQLType + private function getResourceType(Type|LegacyType $type, bool $input, Operation $rootOperation, string $rootResource, ?string $property, int $depth): ?GraphQLType { - if ( - $this->typeBuilder->isCollection($type) - && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null - ) { - $resourceClass = $collectionValueType->getClassName(); + if ($type instanceof Type) { + $isCollection = $type->isSatisfiedBy(fn ($t) => $t instanceof CollectionType); + + if ($isCollection) { + $type = TypeHelper::getCollectionValueType($type); + } + + /** @var class-string|null $resourceClass */ + $resourceClass = null; + $typeIsResourceClass = function (Type $type) use (&$resourceClass): bool { + return $type instanceof ObjectType && $resourceClass = $type->getClassName(); + }; + + if (!$type->isSatisfiedBy($typeIsResourceClass)) { + return null; + } } else { - $resourceClass = $type->getClassName(); - } + $isCollection = $this->typeBuilder->isCollection($type); + if ($isCollection && $collectionValueType = $type->getCollectionValueTypes()[0] ?? null) { + $resourceClass = $collectionValueType->getClassName(); + } else { + $resourceClass = $type->getClassName(); + } - if (null === $resourceClass) { - return null; + if (null === $resourceClass) { + return null; + } } try { @@ -133,22 +185,19 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati if (!$hasGraphQl) { if (is_a($resourceClass, \BackedEnum::class, true)) { - // Remove the condition in API Platform 4. - if ($this->typeBuilder instanceof TypeBuilderEnumInterface || $this->typeBuilder instanceof ContextAwareTypeBuilderInterface) { - $operation = null; - try { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - $operation = $resourceMetadataCollection->getOperation(); - } catch (ResourceClassNotFoundException|OperationNotFoundException) { - } - /** @var Query $enumOperation */ - $enumOperation = (new Query()) - ->withClass($resourceClass) - ->withShortName($operation?->getShortName() ?? (new \ReflectionClass($resourceClass))->getShortName()) - ->withDescription($operation?->getDescription()); - - return $this->typeBuilder->getEnumType($enumOperation); + $operation = null; + try { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + $operation = $resourceMetadataCollection->getOperation(); + } catch (ResourceClassNotFoundException|OperationNotFoundException) { } + /** @var Query $enumOperation */ + $enumOperation = (new Query()) + ->withClass($resourceClass) + ->withShortName($operation?->getShortName() ?? (new \ReflectionClass($resourceClass))->getShortName()) + ->withDescription($operation?->getDescription()); + + return $this->typeBuilder->getEnumType($enumOperation); } return null; @@ -168,7 +217,6 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati } $operationName = $rootOperation->getName(); - $isCollection = $this->typeBuilder->isCollection($type); // We're retrieving the type of a property which is a relation to the root resource. if ($resourceClass !== $rootResource && $rootOperation instanceof Query) { @@ -178,25 +226,27 @@ private function getResourceType(Type $type, bool $input, Operation $rootOperati try { $operation = $resourceMetadataCollection->getOperation($operationName); } catch (OperationNotFoundException) { - $operation = $resourceMetadataCollection->getOperation($isCollection ? 'collection_query' : 'item_query'); + try { + $operation = $resourceMetadataCollection->getOperation($isCollection ? 'collection_query' : 'item_query'); + } catch (OperationNotFoundException) { + throw new OperationNotFoundException(\sprintf('A GraphQl operation named "%s" should exist on the type "%s" as we reference this type in another query.', $isCollection ? 'collection_query' : 'item_query', $resourceClass)); + } } if (!$operation instanceof Operation) { - throw new OperationNotFoundException(); + throw new OperationNotFoundException(\sprintf('A GraphQl operation named "%s" should exist on the type "%s" as we reference this type in another query.', $operationName, $resourceClass)); } - return $this->typeBuilder instanceof ContextAwareTypeBuilderInterface ? - $this->typeBuilder->getResourceObjectType($resourceMetadataCollection, $operation, $propertyMetadata, [ - 'input' => $input, - 'wrapped' => false, - 'depth' => $depth, - ]) : - $this->typeBuilder->getResourceObjectType($resourceClass, $resourceMetadataCollection, $operation, $input, false, $depth); + return $this->typeBuilder->getResourceObjectType($resourceMetadataCollection, $operation, $propertyMetadata, [ + 'input' => $input, + 'wrapped' => false, + 'depth' => $depth, + ]); } private function resolveAstTypeNode(TypeNode $astTypeNode, string $fromType): ?GraphQLType { if ($astTypeNode instanceof NonNullTypeNode) { - /** @var NullableType|null $nullableAstTypeNode */ + /** @var (GraphQLType&NullableType)|null $nullableAstTypeNode */ $nullableAstTypeNode = $this->resolveNullableAstTypeNode($astTypeNode->type, $fromType); return $nullableAstTypeNode ? GraphQLType::nonNull($nullableAstTypeNode) : null; diff --git a/src/GraphQl/Type/TypeConverterInterface.php b/src/GraphQl/Type/TypeConverterInterface.php index 8a7fc1b5d41..99b19837e32 100644 --- a/src/GraphQl/Type/TypeConverterInterface.php +++ b/src/GraphQl/Type/TypeConverterInterface.php @@ -15,20 +15,25 @@ use ApiPlatform\Metadata\GraphQl\Operation; use GraphQL\Type\Definition\Type as GraphQLType; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; /** * Converts a type to its GraphQL equivalent. * * @author Alan Poulain + * + * @method GraphQLType|string|null convertPhpType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth) */ interface TypeConverterInterface { /** + * @deprecated since 4.1, use "convertPhpType" instead + * * Converts a built-in type to its GraphQL equivalent. * A string can be returned for a custom registered type. */ - public function convertType(Type $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth): GraphQLType|string|null; + public function convertType(LegacyType $type, bool $input, Operation $rootOperation, string $resourceClass, string $rootResource, ?string $property, int $depth): GraphQLType|string|null; /** * Resolves a type written with the GraphQL type system to its object representation. diff --git a/src/GraphQl/composer.json b/src/GraphQl/composer.json index 1af7931f975..3857aa27be1 100644 --- a/src/GraphQl/composer.json +++ b/src/GraphQl/composer.json @@ -20,28 +20,23 @@ } ], "require": { - "php": ">=8.1", - "api-platform/metadata": "*@dev || ^3.1", - "api-platform/serializer": "*@dev || ^3.1", - "api-platform/state": "*@dev || ^3.1", - "api-platform/validator": "*@dev || ^3.1", - "symfony/property-info": "^6.4 || ^7.0", - "symfony/serializer": "^6.4 || ^7.0", - "webonyx/graphql-php": "^14.0 || ^15.0", + "php": ">=8.2", + "api-platform/metadata": "^4.2.0@alpha", + "api-platform/state": "^4.2.0@alpha", + "api-platform/serializer": "^4.2.0@alpha", + "symfony/property-info": "^7.1", + "symfony/serializer": "^6.4 || ^7.1", + "symfony/type-info": "^7.3", + "webonyx/graphql-php": "^15.0", "willdurand/negotiation": "^3.1" }, "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "api-platform/validator": "*@dev || ^3.1", - "twig/twig": "^3.7", + "phpspec/prophecy-phpunit": "^2.2", + "api-platform/validator": "^4.1", + "twig/twig": "^1.42.3 || ^2.12 || ^3.0", "symfony/mercure-bundle": "*", - "symfony/phpunit-bridge": "^6.4 || ^7.0", "symfony/routing": "^6.4 || ^7.0", - "symfony/validator": "^6.4 || ^7.0", - "sebastian/comparator": "<5.0", - "api-platform/doctrine-common": "*@dev || ^3.2", - "api-platform/doctrine-odm": "*@dev || ^3.2", - "api-platform/doctrine-orm": "*@dev || ^3.2" + "phpunit/phpunit": "11.5.x-dev" }, "autoload": { "psr-4": { @@ -51,6 +46,11 @@ "/Tests/" ] }, + "suggest": { + "api-platform/doctrine-odm": "To support doctrine ODM state options.", + "api-platform/doctrine-orm": "To support doctrine ORM state options.", + "api-platform/validator": "To support validation." + }, "config": { "preferred-install": { "*": "dist" @@ -64,13 +64,30 @@ }, "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" }, "symfony": { - "require": "^6.4" + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" } }, "scripts": { "test": "./vendor/bin/phpunit" - } + }, + "conflict": { + "symfony/http-client": "<6.4", + "doctrine/inflector": "<2.0" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] } diff --git a/src/GraphQl/phpunit.baseline.xml b/src/GraphQl/phpunit.baseline.xml new file mode 100644 index 00000000000..b0766d7d308 --- /dev/null +++ b/src/GraphQl/phpunit.baseline.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/GraphQl/phpunit.xml.dist b/src/GraphQl/phpunit.xml.dist index 0e1e002f892..151584b3015 100644 --- a/src/GraphQl/phpunit.xml.dist +++ b/src/GraphQl/phpunit.xml.dist @@ -1,30 +1,23 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + trigger_deprecation + + + ./ + + + ./Tests + ./vendor + + diff --git a/src/Hal/.gitattributes b/src/Hal/.gitattributes new file mode 100644 index 00000000000..801f2080d71 --- /dev/null +++ b/src/Hal/.gitattributes @@ -0,0 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/Hal/.github/workflows/close_pr.yml b/src/Hal/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/Hal/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/ParameterValidator/.gitignore b/src/Hal/.gitignore similarity index 100% rename from src/ParameterValidator/.gitignore rename to src/Hal/.gitignore diff --git a/src/Hal/JsonSchema/SchemaFactory.php b/src/Hal/JsonSchema/SchemaFactory.php index 32433f3da55..8075bd40f81 100644 --- a/src/Hal/JsonSchema/SchemaFactory.php +++ b/src/Hal/JsonSchema/SchemaFactory.php @@ -13,10 +13,15 @@ namespace ApiPlatform\Hal\JsonSchema; +use ApiPlatform\JsonSchema\DefinitionNameFactory; +use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface; +use ApiPlatform\JsonSchema\ResourceMetadataTrait; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryAwareInterface; use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\JsonSchema\SchemaUriPrefixTrait; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; /** * Decorator factory which adds HAL properties to the JSON Schema document. @@ -26,6 +31,11 @@ */ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface { + use ResourceMetadataTrait; + use SchemaUriPrefixTrait; + + private const COLLECTION_BASE_SCHEMA_NAME = 'HalCollectionBaseSchema'; + private const HREF_PROP = [ 'href' => [ 'type' => 'string', @@ -44,8 +54,12 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI ], ]; - public function __construct(private readonly SchemaFactoryInterface $schemaFactory) + public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private ?DefinitionNameFactoryInterface $definitionNameFactory = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null) { + if (!$definitionNameFactory) { + $this->definitionNameFactory = new DefinitionNameFactory(); + } + $this->resourceMetadataFactory = $resourceMetadataFactory; if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) { $this->schemaFactory->setSchemaFactory($this); } @@ -56,79 +70,131 @@ public function __construct(private readonly SchemaFactoryInterface $schemaFacto */ public function buildSchema(string $className, string $format = 'jsonhal', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema { - $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); if ('jsonhal' !== $format) { - return $schema; + return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); + } + + if (!$this->isResourceClass($className)) { + $operation = null; + $inputOrOutputClass = null; + $serializerContext ??= []; + } else { + $operation = $this->findOperation($className, $type, $operation, $serializerContext, $format); + $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext); + $serializerContext ??= $this->getSerializerContext($operation, $type); } + if (null === $inputOrOutputClass) { + // input or output disabled + return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); + } + + $schema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, $schema, $serializerContext, $forceCollection); $definitions = $schema->getDefinitions(); - if ($key = $schema->getRootDefinitionKey()) { - $definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []); + $definitionName = $this->definitionNameFactory->create($className, $format, $className, $operation, $serializerContext); + $prefix = $this->getSchemaUriPrefix($schema->getVersion()); + $collectionKey = $schema->getItemsDefinitionKey(); + + // Already computed + if (!$collectionKey && isset($definitions[$definitionName])) { + $schema['$ref'] = $prefix.$definitionName; return $schema; } - if ($key = $schema->getItemsDefinitionKey()) { - $definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []); + + $key = $schema->getRootDefinitionKey() ?? $collectionKey; + + $definitions[$definitionName] = [ + 'allOf' => [ + ['type' => 'object', 'properties' => self::BASE_PROPS], + ['$ref' => $prefix.$key], + ], + ]; + + if (isset($definitions[$key]['description'])) { + $definitions[$definitionName]['description'] = $definitions[$key]['description']; + } + + if (!$collectionKey) { + $schema['$ref'] = $prefix.$definitionName; + + return $schema; } if (($schema['type'] ?? '') === 'array') { - $items = $schema['items']; - unset($schema['items']); - - $schema['type'] = 'object'; - $schema['properties'] = [ - '_embedded' => [ - 'anyOf' => [ - [ + if (!isset($definitions[self::COLLECTION_BASE_SCHEMA_NAME])) { + $definitions[self::COLLECTION_BASE_SCHEMA_NAME] = [ + 'type' => 'object', + 'properties' => [ + '_embedded' => [ + 'anyOf' => [ + [ + 'type' => 'object', + 'properties' => [ + 'item' => [ + 'type' => 'array', + ], + ], + ], + ['type' => 'object'], + ], + ], + 'totalItems' => [ + 'type' => 'integer', + 'minimum' => 0, + ], + 'itemsPerPage' => [ + 'type' => 'integer', + 'minimum' => 0, + ], + '_links' => [ 'type' => 'object', 'properties' => [ - 'item' => [ - 'type' => 'array', - 'items' => $items, + 'self' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, + ], + 'first' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, + ], + 'last' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, + ], + 'next' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, + ], + 'previous' => [ + 'type' => 'object', + 'properties' => self::HREF_PROP, ], ], ], - ['type' => 'object'], ], - ], - 'totalItems' => [ - 'type' => 'integer', - 'minimum' => 0, - ], - 'itemsPerPage' => [ - 'type' => 'integer', - 'minimum' => 0, - ], - '_links' => [ + 'required' => ['_links', '_embedded'], + ]; + } + + unset($schema['items']); + unset($schema['type']); + + $schema['description'] = "$definitionName collection."; + $schema['allOf'] = [ + ['$ref' => $prefix.self::COLLECTION_BASE_SCHEMA_NAME], + [ 'type' => 'object', 'properties' => [ - 'self' => [ - 'type' => 'object', - 'properties' => self::HREF_PROP, - ], - 'first' => [ - 'type' => 'object', - 'properties' => self::HREF_PROP, - ], - 'last' => [ - 'type' => 'object', - 'properties' => self::HREF_PROP, - ], - 'next' => [ - 'type' => 'object', - 'properties' => self::HREF_PROP, - ], - 'previous' => [ - 'type' => 'object', - 'properties' => self::HREF_PROP, + '_embedded' => [ + 'additionalProperties' => [ + 'type' => 'array', + 'items' => ['$ref' => $prefix.$definitionName], + ], ], ], ], ]; - $schema['required'] = [ - '_links', - '_embedded', - ]; return $schema; } diff --git a/src/Hal/README.md b/src/Hal/README.md new file mode 100644 index 00000000000..6b74be666ac --- /dev/null +++ b/src/Hal/README.md @@ -0,0 +1,12 @@ +# API Platform - HAL + +The [HAL (Hypertext Application Language)](https://stateless.group/hal_specification.html) component of the [API Platform](https://api-platform.com) framework. + +[Documentation](https://api-platform.com/docs/core/content-negotiation/) + +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/Hal/Serializer/CollectionNormalizer.php b/src/Hal/Serializer/CollectionNormalizer.php index 8cce319c174..7e796a6f3a7 100644 --- a/src/Hal/Serializer/CollectionNormalizer.php +++ b/src/Hal/Serializer/CollectionNormalizer.php @@ -13,11 +13,10 @@ namespace ApiPlatform\Hal\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\IriHelper; use ApiPlatform\Serializer\AbstractCollectionNormalizer; -use ApiPlatform\Util\IriHelper; use Symfony\Component\Serializer\Exception\UnexpectedValueException; /** @@ -30,7 +29,7 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer { public const FORMAT = 'jsonhal'; - public function __construct(ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) + public function __construct(ResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) { parent::__construct($resourceClassResolver, $pageParameterName, $resourceMetadataFactory); } diff --git a/src/Hal/Serializer/EntrypointNormalizer.php b/src/Hal/Serializer/EntrypointNormalizer.php index 8bea0ac875c..d8c8399c9fd 100644 --- a/src/Hal/Serializer/EntrypointNormalizer.php +++ b/src/Hal/Serializer/EntrypointNormalizer.php @@ -13,30 +13,25 @@ namespace ApiPlatform\Hal\Serializer; -use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; -use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; -use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Documentation\Entrypoint; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Normalizes the API entrypoint. * * @author Kévin Dunglas */ -final class EntrypointNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class EntrypointNormalizer implements NormalizerInterface { public const FORMAT = 'jsonhal'; - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly LegacyUrlGeneratorInterface|UrlGeneratorInterface $urlGenerator) + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface $iriConverter, private readonly UrlGeneratorInterface $urlGenerator) { } @@ -75,25 +70,14 @@ public function normalize(mixed $object, ?string $format = null, array $context */ public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { - return self::FORMAT === $format && ($data instanceof Entrypoint || $data instanceof DocumentationEntrypoint); + return self::FORMAT === $format && $data instanceof Entrypoint; } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { - return self::FORMAT === $format ? [Entrypoint::class => true, DocumentationEntrypoint::class => true] : []; - } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; + return self::FORMAT === $format ? [Entrypoint::class => true] : []; } } diff --git a/src/Hal/Serializer/ItemNormalizer.php b/src/Hal/Serializer/ItemNormalizer.php index e2825de253a..d0a54141329 100644 --- a/src/Hal/Serializer/ItemNormalizer.php +++ b/src/Hal/Serializer/ItemNormalizer.php @@ -13,14 +13,33 @@ namespace ApiPlatform\Hal\Serializer; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; +use ApiPlatform\Metadata\Util\TypeHelper; use ApiPlatform\Serializer\AbstractItemNormalizer; use ApiPlatform\Serializer\CacheKeyTrait; use ApiPlatform\Serializer\ContextTrait; +use ApiPlatform\Serializer\TagCollectorInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\Serializer\Exception\CircularReferenceException; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; +use Symfony\Component\TypeInfo\Type\ObjectType; /** * Converts between objects and array including HAL metadata. @@ -35,9 +54,25 @@ final class ItemNormalizer extends AbstractItemNormalizer public const FORMAT = 'jsonhal'; + protected const HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'hal_circular_reference_limit_counters'; + private array $componentsCache = []; private array $attributesMetadataCache = []; + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, ?TagCollectorInterface $tagCollector = null) + { + $defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER] = function ($object): ?array { + $iri = $this->iriConverter->getIriFromResource($object); + if (null === $iri) { + return null; + } + + return ['_links' => ['self' => ['href' => $iri]]]; + }; + + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector); + } + /** * {@inheritdoc} */ @@ -46,6 +81,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { return self::FORMAT === $format ? parent::getSupportedTypes($format) : []; @@ -67,9 +105,8 @@ public function normalize(mixed $object, ?string $format = null, array $context } $context = $this->initContext($resourceClass, $context); - $iri = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context); - $context['iri'] = $iri; + $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context); $context['object'] = $object; $context['format'] = $format; $context['api_normalize'] = true; @@ -120,7 +157,7 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a /** * {@inheritdoc} */ - protected function getAttributes($object, $format = null, array $context = []): array + protected function getAttributes(object $object, ?string $format = null, array $context = []): array { return $this->getComponents($object, $format, $context)['states']; } @@ -148,15 +185,28 @@ private function getComponents(object $object, ?string $format, array $context): foreach ($attributes as $attribute) { $propertyMetadata = $this->propertyMetadataFactory->create($context['resource_class'], $attribute, $options); - $types = $propertyMetadata->getBuiltinTypes() ?? []; + if (method_exists(PropertyInfoExtractor::class, 'getType')) { + $type = $propertyMetadata->getNativeType(); + $types = $type instanceof CompositeTypeInterface ? $type->getTypes() : (null === $type ? [] : [$type]); + /** @var class-string|null $className */ + $className = null; + } else { + $types = $propertyMetadata->getBuiltinTypes() ?? []; + } // prevent declaring $attribute as attribute if it's already declared as relationship $isRelationship = false; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); + }; foreach ($types as $type) { $isOne = $isMany = false; - if (null !== $type) { + /** @var Type|LegacyType|null $valueType */ + $valueType = null; + + if ($type instanceof LegacyType) { if ($type->isCollection()) { $valueType = $type->getCollectionValueTypes()[0] ?? null; $isMany = null !== $valueType && ($className = $valueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className); @@ -164,6 +214,12 @@ private function getComponents(object $object, ?string $format, array $context): $className = $type->getClassName(); $isOne = $className && $this->resourceClassResolver->isResourceClass($className); } + } elseif ($type instanceof Type) { + if ($type->isSatisfiedBy(fn ($t) => $t instanceof CollectionType)) { + $isMany = TypeHelper::getCollectionValueType($type)?->isSatisfiedBy($typeIsResourceClass); + } else { + $isOne = $type->isSatisfiedBy($typeIsResourceClass); + } } if (!$isOne && !$isMany) { @@ -217,6 +273,10 @@ private function populateRelation(array $data, object $object, ?string $format, { $class = $this->getObjectClass($object); + if ($this->isHalCircularReference($object, $context)) { + return $this->handleHalCircularReference($object, $format, $context); + } + $attributesMetadata = \array_key_exists($class, $this->attributesMetadataCache) ? $this->attributesMetadataCache[$class] : $this->attributesMetadataCache[$class] = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null; @@ -248,14 +308,16 @@ private function populateRelation(array $data, object $object, ?string $format, $attributeValue = $this->getAttributeValue($object, $relation['name'], $format, $childContext); - if (empty($attributeValue)) { + if (empty($attributeValue) && ($context[self::SKIP_NULL_TO_ONE_RELATIONS] ?? $this->defaultContext[self::SKIP_NULL_TO_ONE_RELATIONS] ?? true)) { continue; } if ('one' === $relation['cardinality']) { if ('links' === $type) { - $data[$key][$relationName]['href'] = $this->getRelationIri($attributeValue); - continue; + if (null !== $attributeValue) { + $data[$key][$relationName]['href'] = $this->getRelationIri($attributeValue); + continue; + } } $data[$key][$relationName] = $attributeValue; @@ -320,4 +382,49 @@ private function isMaxDepthReached(array $attributesMetadata, string $class, str return false; } + + /** + * Detects if the configured circular reference limit is reached. + * + * @throws CircularReferenceException + */ + protected function isHalCircularReference(object $object, array &$context): bool + { + $objectHash = spl_object_hash($object); + + $circularReferenceLimit = $context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT]; + if (isset($context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) { + if ($context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) { + unset($context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]); + + return true; + } + + ++$context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]; + } else { + $context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1; + } + + return false; + } + + /** + * Handles a circular reference. + * + * If a circular reference handler is set, it will be called. Otherwise, a + * {@class CircularReferenceException} will be thrown. + * + * @final + * + * @throws CircularReferenceException + */ + protected function handleHalCircularReference(object $object, ?string $format = null, array $context = []): mixed + { + $circularReferenceHandler = $context[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER]; + if ($circularReferenceHandler) { + return $circularReferenceHandler($object, $format, $context); + } + + throw new CircularReferenceException(\sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', get_debug_type($object), $context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT])); + } } diff --git a/src/Hal/Serializer/ObjectNormalizer.php b/src/Hal/Serializer/ObjectNormalizer.php index 9de3609bf6b..ef1d480a811 100644 --- a/src/Hal/Serializer/ObjectNormalizer.php +++ b/src/Hal/Serializer/ObjectNormalizer.php @@ -13,24 +13,20 @@ namespace ApiPlatform\Hal\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Exception\LogicException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Decorates the output with JSON HAL metadata when appropriate, but otherwise * just passes through to the decorated normalizer. */ -final class ObjectNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface +final class ObjectNormalizer implements NormalizerInterface, DenormalizerInterface { public const FORMAT = 'jsonhal'; - public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter) + public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface $iriConverter) { } @@ -42,33 +38,12 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && $this->decorated->supportsNormalization($data, $format, $context); } - public function getSupportedTypes($format): array - { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->decorated, 'getSupportedTypes')) { - return [ - '*' => $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(), - ]; - } - - return self::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : []; - } - /** - * {@inheritdoc} + * @param string|null $format */ - public function hasCacheableSupportsMethod(): bool + public function getSupportedTypes($format): array { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(); + return self::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : []; } /** diff --git a/src/Hal/Tests/Fixtures/ApiResource/Issue5452/ActivableInterface.php b/src/Hal/Tests/Fixtures/ApiResource/Issue5452/ActivableInterface.php new file mode 100644 index 00000000000..7154123e0bc --- /dev/null +++ b/src/Hal/Tests/Fixtures/ApiResource/Issue5452/ActivableInterface.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hal\Tests\Fixtures\ApiResource\Issue5452; + +interface ActivableInterface +{ +} diff --git a/src/Hal/Tests/Fixtures/ApiResource/Issue5452/Author.php b/src/Hal/Tests/Fixtures/ApiResource/Issue5452/Author.php new file mode 100644 index 00000000000..c4bd356a63f --- /dev/null +++ b/src/Hal/Tests/Fixtures/ApiResource/Issue5452/Author.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hal\Tests\Fixtures\ApiResource\Issue5452; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Tests\Fixtures\TestBundle\State\Issue5452\AuthorItemProvider; + +#[ApiResource( + operations: [ + new Get(uriTemplate: '/issue-5452/authors/{id}{._format}', provider: AuthorItemProvider::class), + ] +)] +class Author implements ActivableInterface, TimestampableInterface +{ + public function __construct( + #[ApiProperty(identifier: true)] + public readonly string|int $id, + public readonly string $name, + ) { + } +} diff --git a/src/Hal/Tests/Fixtures/ApiResource/Issue5452/Book.php b/src/Hal/Tests/Fixtures/ApiResource/Issue5452/Book.php new file mode 100644 index 00000000000..6650738a741 --- /dev/null +++ b/src/Hal/Tests/Fixtures/ApiResource/Issue5452/Book.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hal\Tests\Fixtures\ApiResource\Issue5452; + +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Tests\Fixtures\TestBundle\State\Issue5452\BookCollectionProvider; + +#[GetCollection(uriTemplate: '/issue-5452/books{._format}', provider: BookCollectionProvider::class)] +#[Post(uriTemplate: '/issue-5452/books{._format}')] +class Book +{ + // union types + public string|int|null $number = null; + + // simple types + public ?string $isbn = null; + + // intersect types without specific typehint (throw an error: AbstractItemNormalizer line 872) + public ActivableInterface&TimestampableInterface $library; + + /** + * @var Author + */ + // intersect types with PHPDoc + public ActivableInterface&TimestampableInterface $author; +} diff --git a/src/Hal/Tests/Fixtures/ApiResource/Issue5452/Library.php b/src/Hal/Tests/Fixtures/ApiResource/Issue5452/Library.php new file mode 100644 index 00000000000..e22f101bd89 --- /dev/null +++ b/src/Hal/Tests/Fixtures/ApiResource/Issue5452/Library.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hal\Tests\Fixtures\ApiResource\Issue5452; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Tests\Fixtures\TestBundle\State\Issue5452\LibraryItemProvider; + +#[ApiResource( + operations: [ + new Get(uriTemplate: '/issue-5452/libraries/{id}{._format}', provider: LibraryItemProvider::class), + ] +)] +class Library implements ActivableInterface, TimestampableInterface +{ + public function __construct( + #[ApiProperty(identifier: true)] + public readonly string|int $id, + public readonly string $name, + ) { + } +} diff --git a/src/Hal/Tests/Fixtures/ApiResource/Issue5452/TimestampableInterface.php b/src/Hal/Tests/Fixtures/ApiResource/Issue5452/TimestampableInterface.php new file mode 100644 index 00000000000..55f2364f0d4 --- /dev/null +++ b/src/Hal/Tests/Fixtures/ApiResource/Issue5452/TimestampableInterface.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hal\Tests\Fixtures\ApiResource\Issue5452; + +interface TimestampableInterface +{ +} diff --git a/src/Hal/Tests/Fixtures/Dummy.php b/src/Hal/Tests/Fixtures/Dummy.php new file mode 100644 index 00000000000..67569cb2851 --- /dev/null +++ b/src/Hal/Tests/Fixtures/Dummy.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hal\Tests\Fixtures; + +class Dummy +{ + public int $id; + private RelatedDummy $relatedDummy; + private string $name; + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getRelatedDummy(): RelatedDummy + { + return $this->relatedDummy; + } + + public function setRelatedDummy(RelatedDummy $relatedDummy): void + { + $this->relatedDummy = $relatedDummy; + } +} diff --git a/src/Hal/Tests/Fixtures/MaxDepthDummy.php b/src/Hal/Tests/Fixtures/MaxDepthDummy.php new file mode 100644 index 00000000000..bc7ebb68b8e --- /dev/null +++ b/src/Hal/Tests/Fixtures/MaxDepthDummy.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hal\Tests\Fixtures; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\MaxDepth; + +/** + * @author Brian Fox + */ +#[ApiResource(normalizationContext: ['groups' => ['default'], 'enable_max_depth' => true], denormalizationContext: ['groups' => ['default'], 'enable_max_depth' => true], graphQlOperations: [])] +class MaxDepthDummy +{ + #[Groups(['default'])] + public string $id; + + #[Groups(['default'])] + public string $name; + + #[ApiProperty(fetchEager: false)] + #[Groups(['default'])] + #[MaxDepth(1)] + public MaxDepthDummy $child; +} diff --git a/src/Hal/Tests/Fixtures/RelatedDummy.php b/src/Hal/Tests/Fixtures/RelatedDummy.php new file mode 100644 index 00000000000..f5baf362add --- /dev/null +++ b/src/Hal/Tests/Fixtures/RelatedDummy.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hal\Tests\Fixtures; + +class RelatedDummy +{ + public int $id; +} diff --git a/src/Hal/Tests/JsonSchema/SchemaFactoryTest.php b/src/Hal/Tests/JsonSchema/SchemaFactoryTest.php new file mode 100644 index 00000000000..43a3a4a51a2 --- /dev/null +++ b/src/Hal/Tests/JsonSchema/SchemaFactoryTest.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hal\Tests\JsonSchema; + +use ApiPlatform\Hal\JsonSchema\SchemaFactory; +use ApiPlatform\Hal\Tests\Fixtures\Dummy; +use ApiPlatform\JsonSchema\DefinitionNameFactory; +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use PHPUnit\Framework\TestCase; + +class SchemaFactoryTest extends TestCase +{ + private SchemaFactory $schemaFactory; + + protected function setUp(): void + { + $resourceMetadataFactory = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactory->method('create') + ->with(Dummy::class) + ->willReturn( + new ResourceMetadataCollection(Dummy::class, [ + (new ApiResource())->withOperations(new Operations([ + 'get' => (new Get())->withName('get'), + ])), + ]) + ); + + $propertyNameCollectionFactory = $this->createMock(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactory->method('create') + ->with(Dummy::class, ['enable_getter_setter_extraction' => true, 'schema_type' => Schema::TYPE_OUTPUT]) + ->willReturn(new PropertyNameCollection()); + + $propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class); + + $definitionNameFactory = new DefinitionNameFactory(); + + $baseSchemaFactory = new BaseSchemaFactory( + resourceMetadataFactory: $resourceMetadataFactory, + propertyNameCollectionFactory: $propertyNameCollectionFactory, + propertyMetadataFactory: $propertyMetadataFactory, + definitionNameFactory: $definitionNameFactory, + ); + + $this->schemaFactory = new SchemaFactory($baseSchemaFactory); + } + + public function testBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class); + + $this->assertTrue($resultSchema->isDefined()); + $this->assertSame('Dummy.jsonhal', $resultSchema->getRootDefinitionKey()); + } + + public function testCustomFormatBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'json'); + + $this->assertTrue($resultSchema->isDefined()); + $this->assertSame('Dummy', $resultSchema->getRootDefinitionKey()); + } + + public function testHasRootDefinitionKeyBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class); + $definitions = $resultSchema->getDefinitions(); + $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); + + $this->assertTrue(isset($definitions[$rootDefinitionKey])); + $this->assertTrue(isset($definitions[$rootDefinitionKey]['allOf'][0]['properties'])); + $properties = $resultSchema['definitions'][$rootDefinitionKey]['allOf'][0]['properties']; + $this->assertArrayHasKey('_links', $properties); + $this->assertEquals( + [ + 'type' => 'object', + 'properties' => [ + 'self' => [ + 'type' => 'object', + 'properties' => [ + 'href' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + ], + ], + ], + ], + $properties['_links'] + ); + } + + public function testCollection(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonhal', Schema::TYPE_OUTPUT, new GetCollection()); + $this->assertNull($resultSchema->getRootDefinitionKey()); + + $this->assertTrue(isset($resultSchema['definitions']['Dummy.jsonhal'])); + $this->assertTrue(isset($resultSchema['definitions']['HalCollectionBaseSchema'])); + $this->assertTrue(isset($resultSchema['definitions']['Dummy.jsonhal'])); + + foreach ($resultSchema['allOf'] as $schema) { + if (isset($schema['$ref'])) { + $this->assertEquals($schema['$ref'], '#/definitions/HalCollectionBaseSchema'); + continue; + } + + $this->assertArrayHasKey('_embedded', $schema['properties']); + $this->assertEquals('#/definitions/Dummy.jsonhal', $schema['properties']['_embedded']['additionalProperties']['items']['$ref']); + } + + $forceCollectionSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonhal', Schema::TYPE_OUTPUT, null, null, null, true); + $this->assertEquals($forceCollectionSchema, $resultSchema); + } +} diff --git a/src/Hal/Tests/Serializer/CollectionNormalizerTest.php b/src/Hal/Tests/Serializer/CollectionNormalizerTest.php new file mode 100644 index 00000000000..007b922ff4a --- /dev/null +++ b/src/Hal/Tests/Serializer/CollectionNormalizerTest.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Hal\Serializer; + +use ApiPlatform\Hal\Serializer\CollectionNormalizer; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\State\Pagination\PaginatorInterface; +use ApiPlatform\State\Pagination\PartialPaginatorInterface; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * @author Kévin Dunglas + */ +class CollectionNormalizerTest extends TestCase +{ + #[\PHPUnit\Framework\Attributes\Group('legacy')] + public function testSupportsNormalize(): void + { + $resourceClassResolverMock = $this->createMock(ResourceClassResolverInterface::class); + $resourceMetadataFactoryMock = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $normalizer = new CollectionNormalizer($resourceClassResolverMock, 'page', $resourceMetadataFactoryMock); + + $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, ['resource_class' => 'Foo'])); + $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, ['resource_class' => 'Foo', 'api_sub_level' => true])); + $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, [])); + $this->assertTrue($normalizer->supportsNormalization(new \ArrayObject(), CollectionNormalizer::FORMAT, ['resource_class' => 'Foo'])); + $this->assertFalse($normalizer->supportsNormalization([], 'xml', ['resource_class' => 'Foo'])); + $this->assertFalse($normalizer->supportsNormalization(new \ArrayObject(), 'xml', ['resource_class' => 'Foo'])); + + $this->assertEmpty($normalizer->getSupportedTypes('json')); + $this->assertSame([ + 'native-array' => true, + '\Traversable' => true, + ], $normalizer->getSupportedTypes($normalizer::FORMAT)); + } + + public function testNormalizePaginator(): void + { + $this->assertEquals( + [ + '_links' => [ + 'self' => ['href' => '/?page=3'], + 'first' => ['href' => '/?page=1'], + 'last' => ['href' => '/?page=7'], + 'prev' => ['href' => '/?page=2'], + 'next' => ['href' => '/?page=4'], + 'item' => [ + '/me', + ], + ], + '_embedded' => [ + 'item' => [ + [ + '_links' => [ + 'self' => '/me', + ], + 'name' => 'Kévin', + ], + ], + ], + 'totalItems' => 1312, + 'itemsPerPage' => 12, + ], + $this->normalizePaginator() + ); + } + + public function testNormalizePartialPaginator(): void + { + $this->assertEquals( + [ + '_links' => [ + 'self' => ['href' => '/?page=3'], + 'prev' => ['href' => '/?page=2'], + 'next' => ['href' => '/?page=4'], + 'item' => [ + '/me', + ], + ], + '_embedded' => [ + 'item' => [ + [ + '_links' => [ + 'self' => '/me', + ], + 'name' => 'Kévin', + ], + ], + ], + 'itemsPerPage' => 12, + ], + $this->normalizePaginator(true) + ); + } + + private function normalizePaginator(bool $partial = false): array + { + if ($partial) { + $paginator = $this->createMock(PartialPaginatorInterface::class); + } else { + $paginator = $this->createMock(PaginatorInterface::class); + } + + $paginator->method('getCurrentPage')->willReturn(3.); + $paginator->method('getItemsPerPage')->willReturn(12.); + $paginator->method('valid')->willReturnOnConsecutiveCalls(true, false); // @phpstan-ignore-line + $paginator->method('current')->willReturn('foo'); // @phpstan-ignore-line + + if (!$partial) { + $paginator->method('getLastPage')->willReturn(7.); // @phpstan-ignore-line + $paginator->method('getTotalItems')->willReturn(1312.); // @phpstan-ignore-line + } else { + $paginator->method('count')->willReturn(12); + } + + $resourceClassResolverMock = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolverMock->method('getResourceClass')->with($paginator, 'Foo')->willReturn('Foo'); + + $resourceMetadataFactoryMock = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryMock->method('create')->with('Foo')->willReturn(new ResourceMetadataCollection('Foo', [ + (new ApiResource())->withShortName('Foo')->withOperations(new Operations([ + 'bar' => (new GetCollection())->withShortName('Foo'), + ])), + ])); + + $itemNormalizer = $this->createMock(NormalizerInterface::class); + $itemNormalizer->method('normalize')->with('foo', CollectionNormalizer::FORMAT, [ + 'resource_class' => 'Foo', + 'api_sub_level' => true, + 'root_operation_name' => 'bar', + ])->willReturn(['_links' => ['self' => '/me'], 'name' => 'Kévin']); + + $normalizer = new CollectionNormalizer($resourceClassResolverMock, 'page', $resourceMetadataFactoryMock); + $normalizer->setNormalizer($itemNormalizer); + + return $normalizer->normalize($paginator, CollectionNormalizer::FORMAT, [ + 'resource_class' => 'Foo', + 'operation_name' => 'bar', + ]); + } +} diff --git a/src/Hal/Tests/Serializer/EntrypointNormalizerTest.php b/src/Hal/Tests/Serializer/EntrypointNormalizerTest.php new file mode 100644 index 00000000000..1bc0d59972c --- /dev/null +++ b/src/Hal/Tests/Serializer/EntrypointNormalizerTest.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Hal\Serializer; + +use ApiPlatform\Documentation\Entrypoint; +use ApiPlatform\Hal\Serializer\EntrypointNormalizer; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use PHPUnit\Framework\TestCase; + +/** + * @author Kévin Dunglas + */ +class EntrypointNormalizerTest extends TestCase +{ + public function testSupportNormalization(): void + { + $collection = new ResourceNameCollection(); + $entrypoint = new Entrypoint($collection); + + $factoryMock = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $iriConverterMock = $this->createMock(IriConverterInterface::class); + $urlGeneratorMock = $this->createMock(UrlGeneratorInterface::class); + + $normalizer = new EntrypointNormalizer($factoryMock, $iriConverterMock, $urlGeneratorMock); + + $this->assertTrue($normalizer->supportsNormalization($entrypoint, EntrypointNormalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization($entrypoint, 'json')); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), EntrypointNormalizer::FORMAT)); + + $this->assertEmpty($normalizer->getSupportedTypes('json')); + $this->assertSame([Entrypoint::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); + } + + public function testNormalize(): void + { + $collection = new ResourceNameCollection([Dummy::class]); + $entrypoint = new Entrypoint($collection); + $factoryMock = $this->createMock(ResourceMetadataCollectionFactoryInterface::class); + $operation = (new GetCollection())->withShortName('Dummy')->withClass(Dummy::class); + $factoryMock->expects($this->once())->method('create')->with(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [ + (new ApiResource('Dummy')) + ->withShortName('Dummy') + ->withOperations(new Operations([ + 'get' => $operation, + ])), + ])); + + $iriConverterMock = $this->createMock(IriConverterInterface::class); + $iriConverterMock->expects($this->once())->method('getIriFromResource')->with(Dummy::class, UrlGeneratorInterface::ABS_PATH, $operation)->willReturn('/api/dummies'); + + $urlGeneratorMock = $this->createMock(UrlGeneratorInterface::class); + $urlGeneratorMock->expects($this->once())->method('generate')->with('api_entrypoint')->willReturn('/api'); + + $normalizer = new EntrypointNormalizer($factoryMock, $iriConverterMock, $urlGeneratorMock); + + $expected = [ + '_links' => [ + 'self' => [ + 'href' => '/api', + ], + 'dummy' => [ + 'href' => '/api/dummies', + ], + ], + ]; + $this->assertSame($expected, $normalizer->normalize($entrypoint, EntrypointNormalizer::FORMAT)); + } +} diff --git a/src/Hal/Tests/Serializer/ItemNormalizerTest.php b/src/Hal/Tests/Serializer/ItemNormalizerTest.php new file mode 100644 index 00000000000..c576e962df7 --- /dev/null +++ b/src/Hal/Tests/Serializer/ItemNormalizerTest.php @@ -0,0 +1,521 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hal\Tests\Serializer; + +use ApiPlatform\Hal\Serializer\ItemNormalizer; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\ActivableInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\Author; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\Book; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\Library; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5452\TimestampableInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MaxDepthDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\TypeInfo\Type; + +/** + * @author Kévin Dunglas + */ +class ItemNormalizerTest extends TestCase +{ + use ProphecyTrait; + + public function testDoesNotSupportDenormalization(): void + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('jsonhal is a read-only format.'); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $nameConverter = $this->prophesize(NameConverterInterface::class); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + null, + $nameConverter->reveal() + ); + + $this->assertFalse($normalizer->supportsDenormalization('foo', ItemNormalizer::FORMAT)); + $normalizer->denormalize(['foo'], 'Foo'); + } + + #[\PHPUnit\Framework\Attributes\Group('legacy')] + public function testSupportsNormalization(): void + { + $std = new \stdClass(); + $dummy = new Dummy(); + $dummy->setDescription('hello'); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(\stdClass::class)->willReturn(false); + + $nameConverter = $this->prophesize(NameConverterInterface::class); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + null, + $nameConverter->reveal() + ); + + $this->assertTrue($normalizer->supportsNormalization($dummy, $normalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization($dummy, 'xml')); + $this->assertFalse($normalizer->supportsNormalization($std, $normalizer::FORMAT)); + $this->assertEmpty($normalizer->getSupportedTypes('xml')); + $this->assertSame(['object' => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); + } + + public function testNormalize(): void + { + $relatedDummy = new RelatedDummy(); + $dummy = new Dummy(); + $dummy->setName('hello'); + $dummy->setRelatedDummy($relatedDummy); + + $propertyNameCollection = new PropertyNameCollection(['name', 'relatedDummy']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn($propertyNameCollection); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true) + ); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withDescription('')->withReadable(true)->withWritable(false)->withWritableLink(false) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); + $iriConverterProphecy->getIriFromResource($relatedDummy, Argument::cetera())->willReturn('/related-dummies/2'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass($dummy, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass($relatedDummy, RelatedDummy::class)->willReturn(RelatedDummy::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello'); + + $nameConverter = $this->prophesize(NameConverterInterface::class); + $nameConverter->normalize('name', Argument::any(), Argument::any(), Argument::any())->willReturn('name'); + $nameConverter->normalize('relatedDummy', Argument::any(), Argument::any(), Argument::any())->willReturn('related_dummy'); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + null, + $nameConverter->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $expected = [ + '_links' => [ + 'self' => [ + 'href' => '/dummies/1', + ], + 'related_dummy' => [ + 'href' => '/related-dummies/2', + ], + ], + 'name' => 'hello', + ]; + $this->assertEquals($expected, $normalizer->normalize($dummy)); + } + + public function testNormalizeWithUnionIntersectTypes(): void + { + $author = new Author(id: 2, name: 'Isaac Asimov'); + $library = new Library(id: 3, name: 'Le Bâteau Livre'); + $book = new Book(); + $book->author = $author; + $book->library = $library; + + $propertyNameCollection = new PropertyNameCollection(['author', 'library']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Book::class, Argument::type('array'))->willReturn($propertyNameCollection); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Book::class, 'author', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType(Type::intersection(Type::object(ActivableInterface::class), Type::object(TimestampableInterface::class)))->withReadable(true) + ); + $propertyMetadataFactoryProphecy->create(Book::class, 'library', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType(Type::intersection(Type::object(ActivableInterface::class), Type::object(TimestampableInterface::class)))->withReadable(true) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($book, Argument::cetera())->willReturn('/books/1'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Book::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(ActivableInterface::class)->willReturn(false); + $resourceClassResolverProphecy->isResourceClass(TimestampableInterface::class)->willReturn(false); + $resourceClassResolverProphecy->getResourceClass($book, null)->willReturn(Book::class); + $resourceClassResolverProphecy->getResourceClass(null, Book::class)->willReturn(Book::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + + $nameConverter = $this->prophesize(NameConverterInterface::class); + $nameConverter->normalize('author', Argument::any(), Argument::any(), Argument::any())->willReturn('author'); + $nameConverter->normalize('library', Argument::any(), Argument::any(), Argument::any())->willReturn('library'); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + null, + $nameConverter->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $expected = [ + '_links' => [ + 'self' => [ + 'href' => '/books/1', + ], + ], + 'author' => null, + 'library' => null, + ]; + $this->assertEquals($expected, $normalizer->normalize($book)); + } + + public function testNormalizeWithoutCache(): void + { + $relatedDummy = new RelatedDummy(); + $dummy = new Dummy(); + $dummy->setName('hello'); + $dummy->setRelatedDummy($relatedDummy); + + $propertyNameCollection = new PropertyNameCollection(['name', 'relatedDummy']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn($propertyNameCollection); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true) + ); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); + $iriConverterProphecy->getIriFromResource($relatedDummy, Argument::cetera())->willReturn('/related-dummies/2'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass($dummy, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass($relatedDummy, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello'); + + $nameConverter = $this->prophesize(NameConverterInterface::class); + $nameConverter->normalize('name', Argument::any(), Argument::any(), Argument::any())->willReturn('name'); + $nameConverter->normalize('relatedDummy', Argument::any(), Argument::any(), Argument::any())->willReturn('related_dummy'); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + null, + $nameConverter->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $expected = [ + '_links' => [ + 'self' => [ + 'href' => '/dummies/1', + ], + 'related_dummy' => [ + 'href' => '/related-dummies/2', + ], + ], + 'name' => 'hello', + ]; + $this->assertEquals($expected, $normalizer->normalize($dummy, null, ['not_serializable' => function (): void {}])); + } + + public function testMaxDepth(): void + { + $setId = function (MaxDepthDummy $dummy, int $id): void { + $prop = new \ReflectionProperty($dummy, 'id'); + $prop->setAccessible(true); + $prop->setValue($dummy, $id); + }; + + $level1 = new MaxDepthDummy(); + $setId($level1, 1); + $level1->name = 'level 1'; + + $level2 = new MaxDepthDummy(); + $setId($level2, 2); + $level2->name = 'level 2'; + $level1->child = $level2; + + $level3 = new MaxDepthDummy(); + $setId($level3, 3); + $level3->name = 'level 3'; + $level2->child = $level3; + + $propertyNameCollection = new PropertyNameCollection(['id', 'name', 'child']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(MaxDepthDummy::class, Argument::type('array'))->willReturn($propertyNameCollection); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(MaxDepthDummy::class, 'id', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType(Type::int())->withDescription('')->withReadable(true) + ); + $propertyMetadataFactoryProphecy->create(MaxDepthDummy::class, 'name', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true) + ); + $propertyMetadataFactoryProphecy->create(MaxDepthDummy::class, 'child', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType(Type::object(MaxDepthDummy::class))->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(true) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($level1, Argument::cetera())->willReturn('/max_depth_dummies/1'); + $iriConverterProphecy->getIriFromResource($level2, Argument::cetera())->willReturn('/max_depth_dummies/2'); + $iriConverterProphecy->getIriFromResource($level3, Argument::cetera())->willReturn('/max_depth_dummies/3'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($level1, null)->willReturn(MaxDepthDummy::class); + $resourceClassResolverProphecy->getResourceClass($level1, MaxDepthDummy::class)->willReturn(MaxDepthDummy::class); + $resourceClassResolverProphecy->getResourceClass($level2, MaxDepthDummy::class)->willReturn(MaxDepthDummy::class); + $resourceClassResolverProphecy->getResourceClass($level3, MaxDepthDummy::class)->willReturn(MaxDepthDummy::class); + $resourceClassResolverProphecy->getResourceClass(null, MaxDepthDummy::class)->willReturn(MaxDepthDummy::class); + $resourceClassResolverProphecy->isResourceClass(MaxDepthDummy::class)->willReturn(true); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + null, + null, + new ClassMetadataFactory(new AttributeLoader()) + ); + $serializer = new Serializer([$normalizer]); + $normalizer->setSerializer($serializer); + + $expected = [ + '_links' => [ + 'self' => [ + 'href' => '/max_depth_dummies/1', + ], + 'child' => [ + 'href' => '/max_depth_dummies/2', + ], + ], + '_embedded' => [ + 'child' => [ + '_links' => [ + 'self' => [ + 'href' => '/max_depth_dummies/2', + ], + 'child' => [ + 'href' => '/max_depth_dummies/3', + ], + ], + '_embedded' => [ + 'child' => [ + '_links' => [ + 'self' => [ + 'href' => '/max_depth_dummies/3', + ], + ], + 'id' => 3, + 'name' => 'level 3', + ], + ], + 'id' => 2, + 'name' => 'level 2', + ], + ], + 'id' => 1, + 'name' => 'level 1', + ]; + + $this->assertEquals($expected, $normalizer->normalize($level1, ItemNormalizer::FORMAT)); + $this->assertEquals($expected, $normalizer->normalize($level1, ItemNormalizer::FORMAT, [ObjectNormalizer::ENABLE_MAX_DEPTH => false])); + + $expected = [ + '_links' => [ + 'self' => [ + 'href' => '/max_depth_dummies/1', + ], + 'child' => [ + 'href' => '/max_depth_dummies/2', + ], + ], + '_embedded' => [ + 'child' => [ + '_links' => [ + 'self' => [ + 'href' => '/max_depth_dummies/2', + ], + ], + 'id' => 2, + 'name' => 'level 2', + ], + ], + 'id' => 1, + 'name' => 'level 1', + ]; + + $this->assertEquals($expected, $normalizer->normalize($level1, ItemNormalizer::FORMAT, [ObjectNormalizer::ENABLE_MAX_DEPTH => true])); + } + + /** + * @param array $context + * @param array $expected + */ + #[DataProvider('getSkipNullToOneRelationCases')] + public function testSkipNullToOneRelation(array $context, array $expected): void + { + $dummy = new Dummy(); + $dummy->setAlias(null); + $dummy->relatedDummy = null; + + $propertyNameCollection = new PropertyNameCollection(['alias', 'relatedDummy']); + $propertyNameCollectionFactory = $this->createMock(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactory->method('create')->willReturn($propertyNameCollection); + + $propertyMetadataFactory = $this->createMock(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactory->method('create')->willReturnCallback(function ($resourceClass, $propertyName, $groups) { + if ('alias' == $propertyName) { + return (new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true); + } + if ('relatedDummy' == $propertyName) { + return (new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withDescription('')->withReadable(true)->withWritable(false); + } + }); + + $iriConverter = $this->createMock(IriConverterInterface::class); + $iriConverter->method('getIriFromResource')->willReturn('/dummies/1'); + + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolver->method('getResourceClass')->willReturnCallback(function ($resource) { + if ($resource instanceof Dummy) { + return Dummy::class; + } + if (null == $resource) { + return RelatedDummy::class; + } + }); + $resourceClassResolver->method('isResourceClass')->willReturn(true); + + $serializer = $this->createMockForIntersectionOfInterfaces([SerializerInterface::class, NormalizerInterface::class]); + $serializer->method('normalize')->with(null, null, self::anything())->willReturn(null); + + $nameConverter = self::createMock(NameConverterInterface::class); + $nameConverter->method('normalize')->willReturnCallback(function ($propertyName) { + if ('alias' == $propertyName) { + return 'alias'; + } + if ('relatedDummy' == $propertyName) { + return 'related_dummy'; + } + }); + + $normalizer = new ItemNormalizer( + propertyNameCollectionFactory: $propertyNameCollectionFactory, + propertyMetadataFactory: $propertyMetadataFactory, + iriConverter: $iriConverter, + resourceClassResolver: $resourceClassResolver, + propertyAccessor: null, + nameConverter: $nameConverter, + classMetadataFactory: null, + defaultContext: [], + resourceMetadataCollectionFactory: null, + resourceAccessChecker: null, + tagCollector: null, + ); + + $normalizer->setSerializer($serializer); // @phpstan-ignore-line + + self::assertThat($expected, self::equalTo($normalizer->normalize($dummy, null, $context))); + } + + public static function getSkipNullToOneRelationCases(): iterable + { + yield [ + ['skip_null_to_one_relations' => true], + [ + '_links' => [ + 'self' => [ + 'href' => '/dummies/1', + ], + ], + 'alias' => null, + ], + ]; + + yield [ + ['skip_null_to_one_relations' => false], + [ + '_links' => [ + 'self' => [ + 'href' => '/dummies/1', + ], + 'related_dummy' => null, + ], + 'alias' => null, + ]]; + } +} diff --git a/src/Hal/Tests/Serializer/ObjectNormalizerTest.php b/src/Hal/Tests/Serializer/ObjectNormalizerTest.php new file mode 100644 index 00000000000..8053dde307f --- /dev/null +++ b/src/Hal/Tests/Serializer/ObjectNormalizerTest.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Hal\Serializer; + +use ApiPlatform\Hal\Serializer\ObjectNormalizer; +use ApiPlatform\Hal\Tests\Fixtures\Dummy; +use ApiPlatform\Metadata\IriConverterInterface; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Serializer\Exception\LogicException; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * @author Tomasz Grochowski + */ +class ObjectNormalizerTest extends TestCase +{ + public function testDoesNotSupportDenormalization(): void + { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('jsonhal is a read-only format.'); + + $normalizerInterfaceMock = $this->createMock(NormalizerInterface::class); + $iriConverterMock = $this->createMock(IriConverterInterface::class); + + $normalizer = new ObjectNormalizer( + $normalizerInterfaceMock, + $iriConverterMock + ); + + $this->assertFalse($normalizer->supportsDenormalization('foo', 'type', 'format')); + $normalizer->denormalize(['foo'], 'Foo'); + } + + #[\PHPUnit\Framework\Attributes\Group('legacy')] + public function testSupportsNormalization(): void + { + $std = new \stdClass(); + $dummy = new Dummy(); + + $normalizerInterfaceMock = $this->createMock(NormalizerInterface::class); + $iriConverterMock = $this->createMock(IriConverterInterface::class); + $normalizer = new ObjectNormalizer( + $normalizerInterfaceMock, + $iriConverterMock + ); + + $normalizerInterfaceMock->method('supportsNormalization')->willReturn(true); + + $this->assertFalse($normalizer->supportsNormalization($dummy, 'xml')); + $this->assertTrue($normalizer->supportsNormalization($std, ObjectNormalizer::FORMAT)); + $this->assertTrue($normalizer->supportsNormalization($dummy, ObjectNormalizer::FORMAT)); + } +} diff --git a/src/Hal/composer.json b/src/Hal/composer.json new file mode 100644 index 00000000000..6747e346d90 --- /dev/null +++ b/src/Hal/composer.json @@ -0,0 +1,77 @@ +{ + "name": "api-platform/hal", + "description": "API Hal support", + "type": "library", + "keywords": [ + "REST", + "API", + "HAL" + ], + "homepage": "https://api-platform.com/", + "license": "MIT", + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr/" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "require": { + "php": ">=8.2", + "api-platform/state": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/documentation": "^4.1.11", + "api-platform/serializer": "^4.1.11", + "symfony/type-info": "^7.3" + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Hal\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "phpstan/extension-installer": true + } + }, + "extra": { + "branch-alias": { + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" + }, + "symfony": { + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" + } + }, + "scripts": { + "test": "./vendor/bin/phpunit" + }, + "require-dev": { + "phpunit/phpunit": "11.5.x-dev", + "api-platform/json-schema": "^4.1.11" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] +} diff --git a/src/Hal/phpunit.baseline.xml b/src/Hal/phpunit.baseline.xml new file mode 100644 index 00000000000..e3ef6196399 --- /dev/null +++ b/src/Hal/phpunit.baseline.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Hal/phpunit.xml.dist b/src/Hal/phpunit.xml.dist new file mode 100644 index 00000000000..5aa43a61149 --- /dev/null +++ b/src/Hal/phpunit.xml.dist @@ -0,0 +1,23 @@ + + + + + + + + ./Tests/ + + + + + trigger_deprecation + + + ./ + + + ./Tests + ./vendor + + + diff --git a/src/HttpCache/.gitattributes b/src/HttpCache/.gitattributes new file mode 100644 index 00000000000..801f2080d71 --- /dev/null +++ b/src/HttpCache/.gitattributes @@ -0,0 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/HttpCache/.github/workflows/close_pr.yml b/src/HttpCache/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/HttpCache/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/HttpCache/EventListener/AddHeadersListener.php b/src/HttpCache/EventListener/AddHeadersListener.php deleted file mode 100644 index 516ce45e0f4..00000000000 --- a/src/HttpCache/EventListener/AddHeadersListener.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\HttpCache\EventListener; - -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\State\Util\RequestAttributesExtractor; -use Symfony\Component\HttpKernel\Event\ResponseEvent; - -/** - * Configures cache HTTP headers for the current response. - * - * @author Kévin Dunglas - * - * @deprecated use \Symfony\EventListener\AddHeadersListener.php instead - */ -final class AddHeadersListener -{ - use OperationRequestInitiatorTrait; - - public function __construct(private readonly bool $etag = false, private readonly ?int $maxAge = null, private readonly ?int $sharedMaxAge = null, private readonly ?array $vary = null, private readonly ?bool $public = null, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?int $staleWhileRevalidate = null, private readonly ?int $staleIfError = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - if (!$request->isMethodCacheable()) { - return; - } - - $attributes = RequestAttributesExtractor::extractAttributes($request); - if (\count($attributes) < 1) { - return; - } - - $response = $event->getResponse(); - - if (!$response->getContent() || !$response->isSuccessful()) { - return; - } - - $operation = $this->initializeOperation($request); - if ('api_platform.symfony.main_controller' === $operation?->getController()) { - return; - } - $resourceCacheHeaders = $attributes['cache_headers'] ?? $operation?->getCacheHeaders() ?? []; - - if ($this->etag && !$response->getEtag()) { - $response->setEtag(md5((string) $response->getContent())); - } - - if (null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $this->maxAge) && !$response->headers->hasCacheControlDirective('max-age')) { - $response->setMaxAge($maxAge); - } - - $vary = $resourceCacheHeaders['vary'] ?? $this->vary; - if (null !== $vary) { - $response->setVary(array_diff($vary, $response->getVary()), false); - } - - // if the public-property is defined and not yet set; apply it to the response - $public = ($resourceCacheHeaders['public'] ?? $this->public); - if (null !== $public && !$response->headers->hasCacheControlDirective('public')) { - $public ? $response->setPublic() : $response->setPrivate(); - } - - // Cache-Control "s-maxage" is only relevant is resource is not marked as "private" - if (false !== $public && null !== ($sharedMaxAge = $resourceCacheHeaders['shared_max_age'] ?? $this->sharedMaxAge) && !$response->headers->hasCacheControlDirective('s-maxage')) { - $response->setSharedMaxAge($sharedMaxAge); - } - - if (null !== ($staleWhileRevalidate = $resourceCacheHeaders['stale_while_revalidate'] ?? $this->staleWhileRevalidate) && !$response->headers->hasCacheControlDirective('stale-while-revalidate')) { - $response->headers->addCacheControlDirective('stale-while-revalidate', (string) $staleWhileRevalidate); - } - - if (null !== ($staleIfError = $resourceCacheHeaders['stale_if_error'] ?? $this->staleIfError) && !$response->headers->hasCacheControlDirective('stale-if-error')) { - $response->headers->addCacheControlDirective('stale-if-error', (string) $staleIfError); - } - } -} diff --git a/src/HttpCache/EventListener/AddTagsListener.php b/src/HttpCache/EventListener/AddTagsListener.php deleted file mode 100644 index 74cd37ef4ee..00000000000 --- a/src/HttpCache/EventListener/AddTagsListener.php +++ /dev/null @@ -1,95 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\HttpCache\EventListener; - -use ApiPlatform\HttpCache\PurgerInterface; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\State\UriVariablesResolverTrait; -use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\State\Util\RequestAttributesExtractor; -use Symfony\Component\HttpKernel\Event\ResponseEvent; - -/** - * Sets the list of resources' IRIs included in this response in the configured cache tag HTTP header and/or "xkey" HTTP headers. - * - * By default the "Cache-Tags" HTTP header is used because it is supported by CloudFlare. - * - * @see https://developers.cloudflare.com/cache/how-to/purge-cache#add-cache-tag-http-response-headers - * - * The "xkey" is used because it is supported by Varnish. - * @see https://docs.varnish-software.com/varnish-cache-plus/vmods/ykey/ - * - * @author Kévin Dunglas - * - * @deprecated use \Symfony\EventListener\AddTagsListener.php instead - */ -final class AddTagsListener -{ - use OperationRequestInitiatorTrait; - use UriVariablesResolverTrait; - - public function __construct(private readonly IriConverterInterface $iriConverter, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly ?PurgerInterface $purger = null) - { - $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; - } - - /** - * Adds the configured HTTP cache tag and "xkey" headers. - */ - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - $operation = $this->initializeOperation($request); - if ('api_platform.symfony.main_controller' === $operation?->getController()) { - return; - } - $response = $event->getResponse(); - - if ( - !$request->isMethodCacheable() - || !$response->isCacheable() - || (!$attributes = RequestAttributesExtractor::extractAttributes($request)) - ) { - return; - } - - $resources = $request->attributes->get('_resources'); - if ($operation instanceof CollectionOperationInterface) { - // Allows to purge collections - $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $attributes['resource_class']); - $iri = $this->iriConverter->getIriFromResource($attributes['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => $uriVariables]); - - $resources[$iri] = $iri; - } - - if (!$resources) { - return; - } - - if (!$this->purger) { - $response->headers->set('Cache-Tags', implode(',', $resources)); - - return; - } - - $headers = $this->purger->getResponseHeaders($resources); - - foreach ($headers as $key => $value) { - $response->headers->set($key, $value); - } - } -} diff --git a/src/HttpCache/README.md b/src/HttpCache/README.md index f5ebb6814b7..955f70b67cc 100644 --- a/src/HttpCache/README.md +++ b/src/HttpCache/README.md @@ -1,7 +1,15 @@ # API Platform - HTTP Cache -HTTP Cache support. +The [HTTP Cache](https://httpwg.org/specs/rfc7234.html) component of the [API Platform](https://api-platform.com) framework. -## Resources +This component also provides integrations with [Varnish](https://varnish-cache.org/), [Souin](https://souin.io/) +and other HTTP cache servers and services. +[Documentation](https://api-platform.com/docs/core/performance/) +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/HttpCache/SouinPurger.php b/src/HttpCache/SouinPurger.php index 70b3727b5a7..4fc2ff73cca 100644 --- a/src/HttpCache/SouinPurger.php +++ b/src/HttpCache/SouinPurger.php @@ -29,8 +29,8 @@ class SouinPurger extends SurrogateKeysPurger /** * @param HttpClientInterface[] $clients */ - public function __construct(iterable $clients) + public function __construct(iterable $clients, int $maxHeaderLength = self::MAX_HEADER_SIZE_PER_BATCH) { - parent::__construct($clients, self::MAX_HEADER_SIZE_PER_BATCH, self::HEADER, self::SEPARATOR); + parent::__construct($clients, $maxHeaderLength, self::HEADER, self::SEPARATOR); } } diff --git a/src/HttpCache/State/AddHeadersProcessor.php b/src/HttpCache/State/AddHeadersProcessor.php index e1c008b7618..48c431a8adb 100644 --- a/src/HttpCache/State/AddHeadersProcessor.php +++ b/src/HttpCache/State/AddHeadersProcessor.php @@ -29,8 +29,16 @@ final class AddHeadersProcessor implements ProcessorInterface /** * @param ProcessorInterface $decorated */ - public function __construct(private readonly ProcessorInterface $decorated, private readonly bool $etag = false, private readonly ?int $maxAge = null, private readonly ?int $sharedMaxAge = null, private readonly ?array $vary = null, private readonly ?bool $public = null, private readonly ?int $staleWhileRevalidate = null, private readonly ?int $staleIfError = null) - { + public function __construct( + private readonly ProcessorInterface $decorated, + private readonly bool $etag = false, + private readonly ?int $maxAge = null, + private readonly ?int $sharedMaxAge = null, + private readonly ?array $vary = null, + private readonly ?bool $public = null, + private readonly ?int $staleWhileRevalidate = null, + private readonly ?int $staleIfError = null, + ) { } public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed @@ -52,38 +60,31 @@ public function process(mixed $data, Operation $operation, array $uriVariables = $resourceCacheHeaders = $operation->getCacheHeaders() ?? []; - if ($this->etag && !$response->getEtag()) { - $response->setEtag(md5((string) $content)); - } + $public = ($resourceCacheHeaders['public'] ?? $this->public); - if (null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $this->maxAge) && !$response->headers->hasCacheControlDirective('max-age')) { - $response->setMaxAge($maxAge); - } + $options = [ + 'etag' => $this->etag && !$response->getEtag() ? hash('xxh3', (string) $content) : null, + 'max_age' => null !== ($maxAge = $resourceCacheHeaders['max_age'] ?? $this->maxAge) && !$response->headers->hasCacheControlDirective('max-age') ? $maxAge : null, + // Cache-Control "s-maxage" is only relevant is resource is not marked as "private" + 's_maxage' => false !== $public && null !== ($sharedMaxAge = $resourceCacheHeaders['shared_max_age'] ?? $this->sharedMaxAge) && !$response->headers->hasCacheControlDirective('s-maxage') ? $sharedMaxAge : null, + 'public' => null !== $public && !$response->headers->hasCacheControlDirective('public') ? $public : null, + 'stale_while_revalidate' => null !== ($staleWhileRevalidate = $resourceCacheHeaders['stale_while_revalidate'] ?? $this->staleWhileRevalidate) && !$response->headers->hasCacheControlDirective('stale-while-revalidate') ? $staleWhileRevalidate : null, + 'stale_if_error' => null !== ($staleIfError = $resourceCacheHeaders['stale_if_error'] ?? $this->staleIfError) && !$response->headers->hasCacheControlDirective('stale-if-error') ? $staleIfError : null, + 'must_revalidate' => null !== ($mustRevalidate = $resourceCacheHeaders['must_revalidate'] ?? null) && !$response->headers->hasCacheControlDirective('must-revalidate') ? $mustRevalidate : null, + 'proxy_revalidate' => null !== ($proxyRevalidate = $resourceCacheHeaders['proxy_revalidate'] ?? null) && !$response->headers->hasCacheControlDirective('proxy-revalidate') ? $proxyRevalidate : null, + 'no_cache' => null !== ($noCache = $resourceCacheHeaders['no_cache'] ?? null) && !$response->headers->hasCacheControlDirective('no-cache') ? $noCache : null, + 'no_store' => null !== ($noStore = $resourceCacheHeaders['no_store'] ?? null) && !$response->headers->hasCacheControlDirective('no-store') ? $noStore : null, + 'no_transform' => null !== ($noTransform = $resourceCacheHeaders['no_transform'] ?? null) && !$response->headers->hasCacheControlDirective('no-transform') ? $noTransform : null, + 'immutable' => null !== ($immutable = $resourceCacheHeaders['immutable'] ?? null) && !$response->headers->hasCacheControlDirective('immutable') ? $immutable : null, + ]; + + $response->setCache($options); $vary = $resourceCacheHeaders['vary'] ?? $this->vary; if (null !== $vary) { $response->setVary(array_diff($vary, $response->getVary()), false); } - // if the public-property is defined and not yet set; apply it to the response - $public = ($resourceCacheHeaders['public'] ?? $this->public); - if (null !== $public && !$response->headers->hasCacheControlDirective('public')) { - $public ? $response->setPublic() : $response->setPrivate(); - } - - // Cache-Control "s-maxage" is only relevant is resource is not marked as "private" - if (false !== $public && null !== ($sharedMaxAge = $resourceCacheHeaders['shared_max_age'] ?? $this->sharedMaxAge) && !$response->headers->hasCacheControlDirective('s-maxage')) { - $response->setSharedMaxAge($sharedMaxAge); - } - - if (null !== ($staleWhileRevalidate = $resourceCacheHeaders['stale_while_revalidate'] ?? $this->staleWhileRevalidate) && !$response->headers->hasCacheControlDirective('stale-while-revalidate')) { - $response->headers->addCacheControlDirective('stale-while-revalidate', (string) $staleWhileRevalidate); - } - - if (null !== ($staleIfError = $resourceCacheHeaders['stale_if_error'] ?? $this->staleIfError) && !$response->headers->hasCacheControlDirective('stale-if-error')) { - $response->headers->addCacheControlDirective('stale-if-error', (string) $staleIfError); - } - return $response; } } diff --git a/src/HttpCache/Tests/SouinPurgerTest.php b/src/HttpCache/Tests/SouinPurgerTest.php index 1a07617d9da..7bba11d8272 100644 --- a/src/HttpCache/Tests/SouinPurgerTest.php +++ b/src/HttpCache/Tests/SouinPurgerTest.php @@ -14,15 +14,13 @@ namespace ApiPlatform\HttpCache\Tests; use ApiPlatform\HttpCache\SouinPurger; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Promise\PromiseInterface; -use GuzzleHttp\Psr7\Response; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; /** * @author Sylvain Combraque @@ -59,35 +57,24 @@ private function generateXResourcesTags(int $number, int $minimum = 0): array public function testMultiChunkedTags(): void { - /** @var HttpClientInterface $client */ - $client = new class implements ClientInterface { + $client = new class implements HttpClientInterface { public array $sentRegexes = []; - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw new \LogicException('Not implemented'); - } - - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - throw new \LogicException('Not implemented'); - } - - public function request($method, $uri, array $options = []): ResponseInterface + public function request(string $method, string $url, array $options = []): ResponseInterface { $this->sentRegexes[] = $options['headers']['Surrogate-Key']; - return new Response(); + return new MockResponse(); } - public function requestAsync($method, $uri, array $options = []): PromiseInterface + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { throw new \LogicException('Not implemented'); } - public function getConfig($option = null): void + public function withOptions(array $options): static { - throw new \LogicException('Not implemented'); + return $this; } }; $purger = new SouinPurger([$client]); @@ -96,71 +83,49 @@ public function getConfig($option = null): void self::assertSame([ implode(', ', $this->generateXResourcesTags(146)), implode(', ', $this->generateXResourcesTags(200, 146)), - ], $client->sentRegexes); // @phpstan-ignore-line + ], $client->sentRegexes); } public function testPurgeWithMultipleClients(): void { - /** @var HttpClientInterface $client1 */ - $client1 = new class implements ClientInterface { - public $requests = []; + $client1 = new class implements HttpClientInterface { + public array $requests = []; - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw new \LogicException('Not implemented'); - } - - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - throw new \LogicException('Not implemented'); - } - - public function request($method, $uri, array $options = []): ResponseInterface + public function request(string $method, string $url, array $options = []): ResponseInterface { $this->requests[] = [$method, 'http://dummy_host/dummy_api_path/souin_api', $options]; - return new Response(); + return new MockResponse(); } - public function requestAsync($method, $uri, array $options = []): PromiseInterface + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { throw new \LogicException('Not implemented'); } - public function getConfig($option = null): void + public function withOptions(array $options): static { - throw new \LogicException('Not implemented'); + return $this; } }; - /** @var HttpClientInterface $client2 */ - $client2 = new class implements ClientInterface { - public $requests = []; + $client2 = new class implements HttpClientInterface { + public array $requests = []; - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw new \LogicException('Not implemented'); - } - - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - throw new \LogicException('Not implemented'); - } - - public function request($method, $uri, array $options = []): ResponseInterface + public function request(string $method, string $url, array $options = []): ResponseInterface { $this->requests[] = [$method, 'http://dummy_host/dummy_api_path/souin_api', $options]; - return new Response(); + return new MockResponse(); } - public function requestAsync($method, $uri, array $options = []): PromiseInterface + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { throw new \LogicException('Not implemented'); } - public function getConfig($option = null): void + public function withOptions(array $options): static { - throw new \LogicException('Not implemented'); + return $this; } }; @@ -170,12 +135,12 @@ public function getConfig($option = null): void Request::METHOD_PURGE, 'http://dummy_host/dummy_api_path/souin_api', ['headers' => ['Surrogate-Key' => '/foo']], - ], $client1->requests[0]); // @phpstan-ignore-line + ], $client1->requests[0]); self::assertSame([ Request::METHOD_PURGE, 'http://dummy_host/dummy_api_path/souin_api', ['headers' => ['Surrogate-Key' => '/foo']], - ], $client2->requests[0]); // @phpstan-ignore-line + ], $client2->requests[0]); } public function testGetResponseHeaders(): void diff --git a/src/HttpCache/Tests/State/AddHeadersProcessorTest.php b/src/HttpCache/Tests/State/AddHeadersProcessorTest.php index 384ac105ee2..5472b5ec85a 100644 --- a/src/HttpCache/Tests/State/AddHeadersProcessorTest.php +++ b/src/HttpCache/Tests/State/AddHeadersProcessorTest.php @@ -19,38 +19,54 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpFoundation\ResponseHeaderBag; class AddHeadersProcessorTest extends TestCase { - public function testAddHeaders(): void + public function testAddHeadersFromGlobalConfiguration(): void { $operation = new Get(); - $response = $this->createMock(Response::class); - $response->expects($this->once())->method('setEtag'); - $response->method('getContent')->willReturn('{}'); - $response->method('isSuccessful')->willReturn(true); - $response->headers = $this->createMock(ResponseHeaderBag::class); - $response->headers->method('hasCacheControlDirective')->with($this->logicalOr( - $this->identicalTo('public'), - $this->identicalTo('s-maxage'), - $this->identicalTo('max-age'), - $this->identicalTo('stale-while-revalidate'), - $this->identicalTo('stale-if-error'), - ))->willReturn(false); - $response->headers->expects($this->exactly(2))->method('addCacheControlDirective')->with($this->logicalOr( - $this->identicalTo('stale-while-revalidate'), - $this->identicalTo('stale-if-error'), - ), '10'); - $response->expects($this->once())->method('setPublic'); - $response->expects($this->once())->method('setMaxAge'); - $response->expects($this->once())->method('setSharedMaxAge'); - $request = $this->createMock(Request::class); - $request->method('isMethodCacheable')->willReturn(true); + $response = new Response('content'); + $request = new Request(); $context = ['request' => $request]; $decorated = $this->createMock(ProcessorInterface::class); $decorated->method('process')->willReturn($response); $processor = new AddHeadersProcessor($decorated, etag: true, maxAge: 100, sharedMaxAge: 200, vary: ['Accept', 'Accept-Encoding'], public: true, staleWhileRevalidate: 10, staleIfError: 10); + $processor->process($response, $operation, [], $context); + + self::assertSame('max-age=100, public, s-maxage=200, stale-if-error=10, stale-while-revalidate=10', $response->headers->get('cache-control')); + self::assertSame('"55f2b31a6acfaa64"', $response->headers->get('etag')); + self::assertSame(['Accept', 'Accept-Encoding'], $response->headers->all('vary')); + } + + public function testAddHeadersFromOperationConfiguration(): void + { + $operation = new Get( + cacheHeaders: [ + 'public' => false, + 'max_age' => 250, + 'shared_max_age' => 500, + 'stale_while_revalidate' => 30, + 'stale_if_error' => 15, + 'vary' => ['Authorization', 'Accept-Language'], + 'must_revalidate' => true, + 'proxy_revalidate' => true, + 'no_cache' => true, + 'no_store' => true, + 'no_transform' => true, + 'immutable' => true, + ], + ); + $response = new Response('content'); + $request = new Request(); + $context = ['request' => $request]; + $decorated = $this->createMock(ProcessorInterface::class); + $decorated->method('process')->willReturn($response); + $processor = new AddHeadersProcessor($decorated); + + $processor->process($response, $operation, [], $context); + + self::assertSame('immutable, max-age=250, must-revalidate, no-store, no-transform, private, proxy-revalidate, stale-if-error=15, stale-while-revalidate=30', $response->headers->get('cache-control')); + self::assertSame(['Authorization', 'Accept-Language'], $response->headers->all('vary')); } } diff --git a/src/HttpCache/Tests/State/AddTagsProcessorTest.php b/src/HttpCache/Tests/State/AddTagsProcessorTest.php index 4413bda2570..210258e7622 100644 --- a/src/HttpCache/Tests/State/AddTagsProcessorTest.php +++ b/src/HttpCache/Tests/State/AddTagsProcessorTest.php @@ -51,7 +51,7 @@ public function testAddTags(): void public function testAddTagsCollection(): void { - $operation = new GetCollection(class: 'Foo', uriVariables: ['id' => new Link()]); + $operation = new GetCollection(class: \stdClass::class, uriVariables: ['id' => new Link()]); $response = $this->createMock(Response::class); $response->method('isCacheable')->willReturn(true); $response->headers = $this->createMock(ResponseHeaderBag::class); @@ -65,7 +65,7 @@ public function testAddTagsCollection(): void $decorated = $this->createMock(ProcessorInterface::class); $decorated->method('process')->willReturn($response); $iriConverter = $this->createMock(IriConverterInterface::class); - $iriConverter->expects($this->once())->method('getIriFromResource')->with('Foo', UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => ['id' => 1]])->willReturn('/foos/1/bars'); + $iriConverter->expects($this->once())->method('getIriFromResource')->with(\stdClass::class, UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => ['id' => 1]])->willReturn('/foos/1/bars'); $processor = new AddTagsProcessor($decorated, $iriConverter); $processor->process($response, $operation, [], $context); } diff --git a/src/HttpCache/Tests/VarnishPurgerTest.php b/src/HttpCache/Tests/VarnishPurgerTest.php index 39d4f564fcd..9577d81c8d5 100644 --- a/src/HttpCache/Tests/VarnishPurgerTest.php +++ b/src/HttpCache/Tests/VarnishPurgerTest.php @@ -14,15 +14,13 @@ namespace ApiPlatform\HttpCache\Tests; use ApiPlatform\HttpCache\VarnishPurger; -use GuzzleHttp\ClientInterface; -use GuzzleHttp\Promise\PromiseInterface; -use GuzzleHttp\Psr7\Response; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; /** * @author Kévin Dunglas @@ -59,56 +57,42 @@ public function testPurge(): void public function testEmptyTags(): void { - $clientProphecy1 = $this->prophesize(ClientInterface::class); + $clientProphecy1 = $this->prophesize(HttpClientInterface::class); $clientProphecy1->request()->shouldNotBeCalled(); - /** @var HttpClientInterface $client */ $client = $clientProphecy1->reveal(); $purger = new VarnishPurger([$client]); $purger->purge([]); } - /** - * @dataProvider provideChunkHeaderCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideChunkHeaderCases')] public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLength, array $iris, array $regexesToSend): void { - /** @var HttpClientInterface $client */ - $client = new class implements ClientInterface { + $client = new class implements HttpClientInterface { public array $sentRegexes = []; - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw new \LogicException('Not implemented'); - } - - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - throw new \LogicException('Not implemented'); - } - - public function request($method, $uri, array $options = []): ResponseInterface + public function request(string $method, string $url, array $options = []): ResponseInterface { $this->sentRegexes[] = $options['headers']['ApiPlatform-Ban-Regex']; - return new Response(); + return new MockResponse(); } - public function requestAsync($method, $uri, array $options = []): PromiseInterface + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { throw new \LogicException('Not implemented'); } - public function getConfig($option = null): void + public function withOptions(array $options): static { - throw new \LogicException('Not implemented'); + return $this; } }; $purger = new VarnishPurger([$client], $maxHeaderLength); $purger->purge($iris); - self::assertSame($regexesToSend, $client->sentRegexes); // @phpstan-ignore-line + self::assertSame($regexesToSend, $client->sentRegexes); } public static function provideChunkHeaderCases(): \Generator diff --git a/src/HttpCache/Tests/VarnishXKeyPurgerTest.php b/src/HttpCache/Tests/VarnishXKeyPurgerTest.php index df1653007ce..bef8ad2b2bf 100644 --- a/src/HttpCache/Tests/VarnishXKeyPurgerTest.php +++ b/src/HttpCache/Tests/VarnishXKeyPurgerTest.php @@ -15,14 +15,14 @@ use ApiPlatform\HttpCache\VarnishXKeyPurger; use GuzzleHttp\ClientInterface; -use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Response; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; -use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; +use Symfony\Component\HttpClient\Response\MockResponse; use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; +use Symfony\Contracts\HttpClient\ResponseStreamInterface; /** * @author Kévin Dunglas @@ -98,47 +98,34 @@ public function testCustomGlue(): void $purger->purge(['/foo', '/bar', '/baz']); } - /** - * @dataProvider provideChunkHeaderCases - */ + #[\PHPUnit\Framework\Attributes\DataProvider('provideChunkHeaderCases')] public function testItChunksHeaderToAvoidHittingVarnishLimit(int $maxHeaderLength, array $iris, array $keysToSend): void { - /** @var HttpClientInterface $client */ - $client = new class implements ClientInterface { + $client = new class implements HttpClientInterface { public array $sentKeys = []; - public function send(RequestInterface $request, array $options = []): ResponseInterface - { - throw new \LogicException('Not implemented'); - } - - public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface - { - throw new \LogicException('Not implemented'); - } - - public function request($method, $uri, array $options = []): ResponseInterface + public function request(string $method, string $url, array $options = []): ResponseInterface { $this->sentKeys[] = $options['headers']['xkey']; - return new Response(); + return new MockResponse(); } - public function requestAsync($method, $uri, array $options = []): PromiseInterface + public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface { throw new \LogicException('Not implemented'); } - public function getConfig($option = null): void + public function withOptions(array $options): static { - throw new \LogicException('Not implemented'); + return $this; } }; $purger = new VarnishXKeyPurger([$client], $maxHeaderLength); $purger->purge($iris); - self::assertSame($keysToSend, $client->sentKeys); // @phpstan-ignore-line + self::assertSame($keysToSend, $client->sentKeys); } public static function provideChunkHeaderCases(): \Generator diff --git a/src/HttpCache/composer.json b/src/HttpCache/composer.json index cedf81c473b..533e57281f6 100644 --- a/src/HttpCache/composer.json +++ b/src/HttpCache/composer.json @@ -3,8 +3,10 @@ "description": "API Platform HttpCache component", "type": "library", "keywords": [ - "Cache", - "Http" + "REST", + "API", + "cache", + "HTTP" ], "homepage": "https://api-platform.com/", "license": "MIT", @@ -20,18 +22,18 @@ } ], "require": { - "php": ">=8.1", - "api-platform/metadata": "*@dev || ^3.1", - "api-platform/state": "*@dev || ^3.1", + "php": ">=8.2", + "api-platform/metadata": "^4.1.11", + "api-platform/state": "^4.1.11", "symfony/http-foundation": "^6.4 || ^7.0" }, "require-dev": { "guzzlehttp/guzzle": "^6.0 || ^7.0", "symfony/dependency-injection": "^6.4 || ^7.0", - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", + "phpspec/prophecy-phpunit": "^2.2", "symfony/http-client": "^6.4 || ^7.0", - "sebastian/comparator": "<5.0" + "symfony/type-info": "^7.3", + "phpunit/phpunit": "11.5.x-dev" }, "autoload": { "psr-4": { @@ -53,13 +55,26 @@ }, "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" }, "symfony": { - "require": "^6.4" + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" } }, "scripts": { "test": "./vendor/bin/phpunit" - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] } diff --git a/src/HttpCache/phpunit.xml.dist b/src/HttpCache/phpunit.xml.dist index d2a2213bbf7..f9a00ac58bc 100644 --- a/src/HttpCache/phpunit.xml.dist +++ b/src/HttpCache/phpunit.xml.dist @@ -1,31 +1,23 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + trigger_deprecation + + + ./ + + + ./Tests + ./vendor + + - diff --git a/src/Hydra/.gitattributes b/src/Hydra/.gitattributes index ae3c2e1685a..801f2080d71 100644 --- a/src/Hydra/.gitattributes +++ b/src/Hydra/.gitattributes @@ -1,2 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore /.gitignore export-ignore /Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/Hydra/.github/workflows/close_pr.yml b/src/Hydra/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/Hydra/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/Hydra/Collection.php b/src/Hydra/Collection.php new file mode 100644 index 00000000000..98fb833b912 --- /dev/null +++ b/src/Hydra/Collection.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra; + +use Symfony\Component\JsonStreamer\Attribute\StreamedName; + +/** + * @template T + * + * @internal + */ +class Collection +{ + #[StreamedName('@context')] + public string $context = 'VIRTUAL'; + + #[StreamedName('@id')] + public string $id = 'VIRTUAL'; + + #[StreamedName('@type')] + public string $type = 'Collection'; + + public float $totalItems; + + public ?IriTemplate $search = null; + public ?PartialCollectionView $view = null; + + /** + * @var list + */ + public iterable $member; +} diff --git a/src/Hydra/EventListener/AddLinkHeaderListener.php b/src/Hydra/EventListener/AddLinkHeaderListener.php deleted file mode 100644 index 397ab5ea2c4..00000000000 --- a/src/Hydra/EventListener/AddLinkHeaderListener.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Hydra\EventListener; - -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; -use ApiPlatform\JsonLd\ContextBuilder; -use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\State\Util\CorsTrait; -use Psr\Link\EvolvableLinkProviderInterface; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use Symfony\Component\WebLink\GenericLinkProvider; -use Symfony\Component\WebLink\Link; - -/** - * Adds the HTTP Link header pointing to the Hydra documentation. - * - * @deprecated use ApiPlatform\Hydra\State\HydraLinkProcessor instead - * - * @author Kévin Dunglas - */ -final class AddLinkHeaderListener -{ - use CorsTrait; - - public function __construct(private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator) - { - } - - /** - * Sends the Hydra header on each response. - */ - public function onKernelResponse(ResponseEvent $event): void - { - $request = $event->getRequest(); - if (($operation = $request->attributes->get('_api_operation')) && 'api_platform.symfony.main_controller' === $operation->getController()) { - return; - } - - // Prevent issues with NelmioCorsBundle - if ($this->isPreflightRequest($request)) { - return; - } - - $apiDocUrl = $this->urlGenerator->generate('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL); - $apiDocLink = new Link(ContextBuilder::HYDRA_NS.'apiDocumentation', $apiDocUrl); - $linkProvider = $request->attributes->get('_api_platform_links', new GenericLinkProvider()); - - if (!$linkProvider instanceof EvolvableLinkProviderInterface) { - return; - } - - foreach ($linkProvider->getLinks() as $link) { - if ($link->getHref() === $apiDocUrl) { - return; - } - } - - $request->attributes->set('_api_platform_links', $linkProvider->withLink($apiDocLink)); - } -} diff --git a/src/Hydra/IriTemplate.php b/src/Hydra/IriTemplate.php new file mode 100644 index 00000000000..78cb9782cc8 --- /dev/null +++ b/src/Hydra/IriTemplate.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra; + +use Symfony\Component\JsonStreamer\Attribute\StreamedName; +use Symfony\Component\Serializer\Annotation\SerializedName; + +final class IriTemplate +{ + #[StreamedName('@type')] + #[SerializedName('@type')] + public string $type = 'IriTemplate'; + + public function __construct( + public string $variableRepresentation, + /** @var list */ + public array $mapping = [], + public ?string $template = null, + ) { + } +} diff --git a/src/Hydra/IriTemplateMapping.php b/src/Hydra/IriTemplateMapping.php new file mode 100644 index 00000000000..9cbb7502752 --- /dev/null +++ b/src/Hydra/IriTemplateMapping.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra; + +use Symfony\Component\JsonStreamer\Attribute\StreamedName; +use Symfony\Component\Serializer\Annotation\SerializedName; + +class IriTemplateMapping +{ + #[StreamedName('@type')] + #[SerializedName('@type')] + public string $type = 'IriTemplateMapping'; + + public function __construct( + public string $variable, + public ?string $property, + public bool $required = false, + ) { + } +} diff --git a/src/Hydra/JsonSchema/SchemaFactory.php b/src/Hydra/JsonSchema/SchemaFactory.php index f51af53e3d7..cd7eed0dcda 100644 --- a/src/Hydra/JsonSchema/SchemaFactory.php +++ b/src/Hydra/JsonSchema/SchemaFactory.php @@ -14,10 +14,16 @@ namespace ApiPlatform\Hydra\JsonSchema; use ApiPlatform\JsonLd\ContextBuilder; +use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; +use ApiPlatform\JsonSchema\DefinitionNameFactory; +use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface; +use ApiPlatform\JsonSchema\ResourceMetadataTrait; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryAwareInterface; use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\JsonSchema\SchemaUriPrefixTrait; use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; /** * Decorator factory which adds Hydra properties to the JSON Schema document. @@ -26,39 +32,73 @@ */ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface { + use HydraPrefixTrait; + use ResourceMetadataTrait; + use SchemaUriPrefixTrait; + + private const ITEM_BASE_SCHEMA_NAME = 'HydraItemBaseSchema'; + private const ITEM_WITHOUT_ID_BASE_SCHEMA_NAME = 'HydraItemBaseSchemaWithoutId'; + private const COLLECTION_BASE_SCHEMA_NAME = 'HydraCollectionBaseSchema'; + private const BASE_PROP = [ - 'readOnly' => true, 'type' => 'string', ]; private const BASE_PROPS = [ '@id' => self::BASE_PROP, '@type' => self::BASE_PROP, ]; - private const BASE_ROOT_PROPS = [ - '@context' => [ - 'readOnly' => true, - 'oneOf' => [ - ['type' => 'string'], - [ - 'type' => 'object', - 'properties' => [ - '@vocab' => [ - 'type' => 'string', - ], - 'hydra' => [ - 'type' => 'string', - 'enum' => [ContextBuilder::HYDRA_NS], + private const ITEM_BASE_SCHEMA = [ + 'type' => 'object', + 'properties' => [ + '@context' => [ + 'oneOf' => [ + ['type' => 'string'], + [ + 'type' => 'object', + 'properties' => [ + '@vocab' => [ + 'type' => 'string', + ], + 'hydra' => [ + 'type' => 'string', + 'enum' => [ContextBuilder::HYDRA_NS], + ], ], + 'required' => ['@vocab', 'hydra'], + 'additionalProperties' => true, ], - 'required' => ['@vocab', 'hydra'], - 'additionalProperties' => true, ], ], - ], - ] + self::BASE_PROPS; + ] + self::BASE_PROPS, + ]; + + private const ITEM_BASE_SCHEMA_WITH_ID = self::ITEM_BASE_SCHEMA + [ + 'required' => ['@id', '@type'], + ]; + + private const ITEM_BASE_SCHEMA_WITHOUT_ID = self::ITEM_BASE_SCHEMA + [ + 'required' => ['@type'], + ]; + + /** + * @var array + */ + private array $transformed = []; + + /** + * @param array $defaultContext + */ + public function __construct( + private readonly SchemaFactoryInterface $schemaFactory, + private readonly array $defaultContext = [], + private ?DefinitionNameFactoryInterface $definitionNameFactory = null, + ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, + ) { + if (!$definitionNameFactory) { + $this->definitionNameFactory = new DefinitionNameFactory(); + } + $this->resourceMetadataFactory = $resourceMetadataFactory; - public function __construct(private readonly SchemaFactoryInterface $schemaFactory) - { if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) { $this->schemaFactory->setSchemaFactory($this); } @@ -69,30 +109,53 @@ public function __construct(private readonly SchemaFactoryInterface $schemaFacto */ public function buildSchema(string $className, string $format = 'jsonld', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema { - $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); - if ('jsonld' !== $format) { - return $schema; + // The input schema must not include `@id` or `@type` as required fields, so it should be a pure JSON schema. + // Strictly speaking, it is possible to include `@id` or `@context` in the input, + // but the generated JSON Schema does not include `"additionalProperties": false` by default, + // so it is possible to include `@id` or `@context` in the input even if the input schema is a JSON schema. + if (Schema::TYPE_INPUT === $type) { + $format = 'json'; } - if ('input' === $type) { - return $schema; + if ('jsonld' !== $format || !$this->isResourceClass($className)) { + return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); } + $operation = $this->findOperation($className, $type, $operation, $serializerContext, $format); + $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext); + $serializerContext ??= $this->getSerializerContext($operation, $type); + + if (null === $inputOrOutputClass) { + // input or output disabled + return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); + } + + $schema = $this->schemaFactory->buildSchema($className, 'jsonld', $type, $operation, $schema, $serializerContext, $forceCollection); $definitions = $schema->getDefinitions(); - if ($key = $schema->getRootDefinitionKey()) { - $definitions[$key]['properties'] = self::BASE_ROOT_PROPS + ($definitions[$key]['properties'] ?? []); + $prefix = $this->getSchemaUriPrefix($schema->getVersion()); + $collectionKey = $schema->getItemsDefinitionKey(); + + if (!$collectionKey) { + $definitionName = $schema->getRootDefinitionKey() ?? $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext); + $this->decorateItemDefinition($definitionName, $definitions, $prefix, $type, $serializerContext); + + if (isset($definitions[$definitionName])) { + $currentDefinitions = $schema->getDefinitions(); + $schema->exchangeArray([]); // Clear the schema + $schema['$ref'] = $prefix.$definitionName; + $schema->setDefinitions($currentDefinitions); + } return $schema; } - if ($key = $schema->getItemsDefinitionKey()) { - $definitions[$key]['properties'] = self::BASE_PROPS + ($definitions[$key]['properties'] ?? []); + + if (($schema['type'] ?? '') !== 'array') { + return $schema; } - if (($schema['type'] ?? '') === 'array') { - // hydra:collection - $items = $schema['items']; - unset($schema['items']); + $hydraPrefix = $this->getHydraPrefix($serializerContext + $this->defaultContext); + if (!isset($definitions[self::COLLECTION_BASE_SCHEMA_NAME])) { switch ($schema->getVersion()) { // JSON Schema + OpenAPI 3.1 case Schema::VERSION_OPENAPI: @@ -105,78 +168,100 @@ public function buildSchema(string $className, string $format = 'jsonld', string break; } - $schema['type'] = 'object'; - $schema['properties'] = [ - 'hydra:member' => [ - 'type' => 'array', - 'items' => $items, + $definitions[self::COLLECTION_BASE_SCHEMA_NAME] = [ + 'type' => 'object', + 'required' => [ + $hydraPrefix.'member', ], - 'hydra:totalItems' => [ - 'type' => 'integer', - 'minimum' => 0, - ], - 'hydra:view' => [ - 'type' => 'object', - 'properties' => [ - '@id' => [ - 'type' => 'string', - 'format' => 'iri-reference', - ], - '@type' => [ - 'type' => 'string', - ], - 'hydra:first' => [ - 'type' => 'string', - 'format' => 'iri-reference', - ], - 'hydra:last' => [ - 'type' => 'string', - 'format' => 'iri-reference', - ], - 'hydra:previous' => [ - 'type' => 'string', - 'format' => 'iri-reference', + 'properties' => [ + $hydraPrefix.'member' => [ + 'type' => 'array', + 'items' => ['type' => 'object'], + ], + $hydraPrefix.'totalItems' => [ + 'type' => 'integer', + 'minimum' => 0, + ], + $hydraPrefix.'view' => [ + 'type' => 'object', + 'properties' => [ + '@id' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + '@type' => [ + 'type' => 'string', + ], + $hydraPrefix.'first' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + $hydraPrefix.'last' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + $hydraPrefix.'previous' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], + $hydraPrefix.'next' => [ + 'type' => 'string', + 'format' => 'iri-reference', + ], ], - 'hydra:next' => [ + 'example' => [ + '@id' => 'string', 'type' => 'string', - 'format' => 'iri-reference', + $hydraPrefix.'first' => 'string', + $hydraPrefix.'last' => 'string', + $hydraPrefix.'previous' => 'string', + $hydraPrefix.'next' => 'string', ], ], - 'example' => [ - '@id' => 'string', - 'type' => 'string', - 'hydra:first' => 'string', - 'hydra:last' => 'string', - 'hydra:previous' => 'string', - 'hydra:next' => 'string', - ], - ], - 'hydra:search' => [ - 'type' => 'object', - 'properties' => [ - '@type' => ['type' => 'string'], - 'hydra:template' => ['type' => 'string'], - 'hydra:variableRepresentation' => ['type' => 'string'], - 'hydra:mapping' => [ - 'type' => 'array', - 'items' => [ - 'type' => 'object', - 'properties' => [ - '@type' => ['type' => 'string'], - 'variable' => ['type' => 'string'], - 'property' => $nullableStringDefinition, - 'required' => ['type' => 'boolean'], + $hydraPrefix.'search' => [ + 'type' => 'object', + 'properties' => [ + '@type' => ['type' => 'string'], + $hydraPrefix.'template' => ['type' => 'string'], + $hydraPrefix.'variableRepresentation' => ['type' => 'string'], + $hydraPrefix.'mapping' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + '@type' => ['type' => 'string'], + 'variable' => ['type' => 'string'], + 'property' => $nullableStringDefinition, + 'required' => ['type' => 'boolean'], + ], ], ], ], ], ], ]; - $schema['required'] = [ - 'hydra:member', - ]; + } - return $schema; + $definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext); + $schema['type'] = 'object'; + $schema['description'] = "$definitionName collection."; + $schema['allOf'] = [ + ['$ref' => $prefix.self::COLLECTION_BASE_SCHEMA_NAME], + [ + 'type' => 'object', + 'properties' => [ + $hydraPrefix.'member' => [ + 'type' => 'array', + 'items' => $schema['items'], + ], + ], + ], + ]; + + unset($schema['items']); + + if (isset($definitions[$collectionKey])) { + $this->decorateItemDefinition($collectionKey, $definitions, $prefix, $type, $serializerContext); } return $schema; @@ -188,4 +273,35 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void $this->schemaFactory->setSchemaFactory($schemaFactory); } } + + private function decorateItemDefinition(string $definitionName, \ArrayObject $definitions, string $prefix, string $type, ?array $serializerContext): void + { + if (!isset($definitions[$definitionName]) || ($this->transformed[$definitionName] ?? false)) { + return; + } + + $hasNoId = Schema::TYPE_OUTPUT === $type && false === ($serializerContext['gen_id'] ?? true); + $baseName = self::ITEM_BASE_SCHEMA_NAME; + if ($hasNoId) { + $baseName = self::ITEM_WITHOUT_ID_BASE_SCHEMA_NAME; + } + + if (!isset($definitions[$baseName])) { + $definitions[$baseName] = $hasNoId ? self::ITEM_BASE_SCHEMA_WITHOUT_ID : self::ITEM_BASE_SCHEMA_WITH_ID; + } + + $allOf = new \ArrayObject(['allOf' => [ + ['$ref' => $prefix.$baseName], + $definitions[$definitionName], + ]]); + + if (isset($definitions[$definitionName]['description'])) { + $allOf['description'] = $definitions[$definitionName]['description']; + } + + $definitions[$definitionName] = $allOf; + unset($definitions[$definitionName]['allOf'][1]['description']); + + $this->transformed[$definitionName] = true; + } } diff --git a/src/Hydra/PartialCollectionView.php b/src/Hydra/PartialCollectionView.php new file mode 100644 index 00000000000..61ae09c9aab --- /dev/null +++ b/src/Hydra/PartialCollectionView.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra; + +use Symfony\Component\JsonStreamer\Attribute\StreamedName; + +class PartialCollectionView +{ + #[StreamedName('@type')] + public string $type = 'PartialCollectionView'; + + public function __construct( + #[StreamedName('@id')] + public string $id, + #[StreamedName('first')] + public ?string $first = null, + #[StreamedName('last')] + public ?string $last = null, + #[StreamedName('previous')] + public ?string $previous = null, + #[StreamedName('next')] + public ?string $next = null, + ) { + } +} diff --git a/src/Hydra/README.md b/src/Hydra/README.md new file mode 100644 index 00000000000..73453153077 --- /dev/null +++ b/src/Hydra/README.md @@ -0,0 +1,14 @@ +# API Platform - Hydra + +The [Hydra](https://www.hydra-cg.com/) component of the [API Platform](https://api-platform.com) framework. + +Hydra simplifies the development of interoperable, hypermedia-driven Web APIs. + +[Documentation](https://api-platform.com/docs/core/) + +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/Hydra/Serializer/CollectionFiltersNormalizer.php b/src/Hydra/Serializer/CollectionFiltersNormalizer.php index 953294e1ca2..c527a32f544 100644 --- a/src/Hydra/Serializer/CollectionFiltersNormalizer.php +++ b/src/Hydra/Serializer/CollectionFiltersNormalizer.php @@ -13,38 +13,42 @@ namespace ApiPlatform\Hydra\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; -use ApiPlatform\Doctrine\Odm\State\Options as ODMOptions; -use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Hydra\IriTemplateMapping; +use ApiPlatform\Hydra\State\Util\SearchHelperTrait; +use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; use ApiPlatform\Metadata\FilterInterface; -use ApiPlatform\Metadata\Parameter; -use ApiPlatform\Metadata\Parameters; -use ApiPlatform\Metadata\QueryParameterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; +use ApiPlatform\State\Util\StateOptionsTrait; use Psr\Container\ContainerInterface; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Enhances the result of collection by adding the filters applied on collection. * * @author Samuel ROZE */ -final class CollectionFiltersNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface +final class CollectionFiltersNormalizer implements NormalizerInterface, NormalizerAwareInterface { + use HydraPrefixTrait; + use SearchHelperTrait; + use StateOptionsTrait; private ?ContainerInterface $filterLocator = null; /** - * @param ContainerInterface $filterLocator The new filter locator or the deprecated filter collection + * @param ContainerInterface $filterLocator The new filter locator or the deprecated filter collection + * @param array $defaultContext */ - public function __construct(private readonly NormalizerInterface $collectionNormalizer, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver, ContainerInterface $filterLocator) - { + public function __construct( + private readonly NormalizerInterface $collectionNormalizer, + private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, + private readonly ResourceClassResolverInterface $resourceClassResolver, + ?ContainerInterface $filterLocator = null, + private readonly array $defaultContext = [], + ) { $this->filterLocator = $filterLocator; } @@ -56,30 +60,14 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return $this->collectionNormalizer->supportsNormalization($data, $format, $context); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->collectionNormalizer, 'getSupportedTypes')) { - return ['*' => $this->collectionNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod()]; - } - return $this->collectionNormalizer->getSupportedTypes($format); } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->collectionNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} */ @@ -117,18 +105,17 @@ public function normalize(mixed $object, ?string $format = null, array $context } } - if ($options = $operation->getStateOptions()) { - if ($options instanceof Options && $options->getEntityClass()) { - $resourceClass = $options->getEntityClass(); - } - - if ($options instanceof ODMOptions && $options->getDocumentClass()) { - $resourceClass = $options->getDocumentClass(); - } - } + $resourceClass = $this->getStateOptionsClass($operation, $resourceClass); if ($currentFilters || ($parameters && \count($parameters))) { - $data['hydra:search'] = $this->getSearch($resourceClass, $requestParts, $currentFilters, $parameters); + $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); + ['mapping' => $mapping, 'keys' => $keys] = $this->getSearchMappingAndKeys($operation, $resourceClass, $currentFilters, $parameters, [$this, 'getFilter']); + $data[$hydraPrefix.'search'] = [ + '@type' => $hydraPrefix.'IriTemplate', + $hydraPrefix.'template' => \sprintf('%s{?%s}', $requestParts['path'], implode(',', $keys)), + $hydraPrefix.'variableRepresentation' => 'BasicRepresentation', + $hydraPrefix.'mapping' => $this->convertMappingToArray($mapping), + ]; } return $data; @@ -145,62 +132,28 @@ public function setNormalizer(NormalizerInterface $normalizer): void } /** - * Returns the content of the Hydra search property. + * @param list $mapping * - * @param FilterInterface[] $filters - * @param array $parameters + * @return array> */ - private function getSearch(string $resourceClass, array $parts, array $filters, array|Parameters|null $parameters): array + private function convertMappingToArray(array $mapping): array { - $variables = []; - $mapping = []; - foreach ($filters as $filter) { - foreach ($filter->getDescription($resourceClass) as $variable => $data) { - $variables[] = $variable; - $mapping[] = ['@type' => 'IriTemplateMapping', 'variable' => $variable, 'property' => $data['property'] ?? null, 'required' => $data['required'] ?? false]; - } - } - - foreach ($parameters ?? [] as $key => $parameter) { - // Each IriTemplateMapping maps a variable used in the template to a property - if (!$parameter instanceof QueryParameterInterface) { - continue; + $convertedMapping = []; + foreach ($mapping as $m) { + $converted = [ + '@type' => 'IriTemplateMapping', + 'variable' => $m->variable, + 'property' => $m->property, + ]; + + if (null !== ($r = $m->required)) { + $converted['required'] = $r; } - if (!($property = $parameter->getProperty()) && ($filterId = $parameter->getFilter()) && ($filter = $this->getFilter($filterId))) { - foreach ($filter->getDescription($resourceClass) as $variable => $description) { - // This is a practice induced by PHP and is not necessary when implementing URI template - if (str_ends_with((string) $variable, '[]')) { - continue; - } - - // :property is a pattern allowed when defining parameters - $k = str_replace(':property', $description['property'], $key); - $variable = str_replace($description['property'], $k, $variable); - $variables[] = $variable; - $m = ['@type' => 'IriTemplateMapping', 'variable' => $variable, 'property' => $description['property'], 'required' => $description['required']]; - if (null !== ($required = $parameter->getRequired())) { - $m['required'] = $required; - } - $mapping[] = $m; - } - - continue; - } - - if (!$property) { - continue; - } - - $m = ['@type' => 'IriTemplateMapping', 'variable' => $key, 'property' => $property]; - $variables[] = $key; - if (null !== ($required = $parameter->getRequired())) { - $m['required'] = $required; - } - $mapping[] = $m; + $convertedMapping[] = $converted; } - return ['@type' => 'hydra:IriTemplate', 'hydra:template' => \sprintf('%s{?%s}', $parts['path'], implode(',', $variables)), 'hydra:variableRepresentation' => 'BasicRepresentation', 'hydra:mapping' => $mapping]; + return $convertedMapping; } /** diff --git a/src/Hydra/Serializer/CollectionNormalizer.php b/src/Hydra/Serializer/CollectionNormalizer.php index 2241ad45cdb..4f6343b5511 100644 --- a/src/Hydra/Serializer/CollectionNormalizer.php +++ b/src/Hydra/Serializer/CollectionNormalizer.php @@ -13,12 +13,10 @@ namespace ApiPlatform\Hydra\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\JsonLd\ContextBuilderInterface; +use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; use ApiPlatform\JsonLd\Serializer\JsonLdContextTrait; use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Serializer\AbstractCollectionNormalizer; @@ -33,6 +31,7 @@ */ final class CollectionNormalizer extends AbstractCollectionNormalizer { + use HydraPrefixTrait; use JsonLdContextTrait; public const FORMAT = 'jsonld'; @@ -41,14 +40,10 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer self::IRI_ONLY => false, ]; - public function __construct(private readonly ContextBuilderInterface $contextBuilder, LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver, private readonly LegacyIriConverterInterface|IriConverterInterface $iriConverter, readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, array $defaultContext = []) + public function __construct(private readonly ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, array $defaultContext = []) { $this->defaultContext = array_merge($this->defaultContext, $defaultContext); - if ($resourceMetadataCollectionFactory) { - trigger_deprecation('api-platform/core', '3.0', \sprintf('Injecting "%s" within "%s" is not needed anymore and this dependency will be removed in 4.0.', ResourceMetadataCollectionFactoryInterface::class, self::class)); - } - parent::__construct($resourceClassResolver, ''); } @@ -58,18 +53,19 @@ public function __construct(private readonly ContextBuilderInterface $contextBui protected function getPaginationData(iterable $object, array $context = []): array { $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class']); + $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); // This adds "jsonld_has_context" by reference, we moved the code to this class. // To follow a note I wrote in the ItemNormalizer, we need to change the JSON-LD context generation as it is more complicated then it should. $data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context); $data['@id'] = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context); - $data['@type'] = 'hydra:Collection'; + $data['@type'] = $hydraPrefix.'Collection'; if ($object instanceof PaginatorInterface) { - $data['hydra:totalItems'] = $object->getTotalItems(); + $data[$hydraPrefix.'totalItems'] = $object->getTotalItems(); } if (\is_array($object) || ($object instanceof \Countable && !$object instanceof PartialPaginatorInterface)) { - $data['hydra:totalItems'] = \count($object); + $data[$hydraPrefix.'totalItems'] = \count($object); } return $data; @@ -80,15 +76,15 @@ protected function getPaginationData(iterable $object, array $context = []): arr */ protected function getItemsData(iterable $object, ?string $format = null, array $context = []): array { - $data = []; - $data['hydra:member'] = []; + $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); + $data = [$hydraPrefix.'member' => []]; $iriOnly = $context[self::IRI_ONLY] ?? $this->defaultContext[self::IRI_ONLY]; foreach ($object as $obj) { if ($iriOnly) { - $data['hydra:member'][] = $this->iriConverter->getIriFromResource($obj); + $data[$hydraPrefix.'member'][] = $this->iriConverter->getIriFromResource($obj); } else { - $data['hydra:member'][] = $this->normalizer->normalize($obj, $format, $context + ['jsonld_has_context' => true]); + $data[$hydraPrefix.'member'][] = $this->normalizer->normalize($obj, $format, $context + ['jsonld_has_context' => true]); } } diff --git a/src/Hydra/Serializer/ConstraintViolationListNormalizer.php b/src/Hydra/Serializer/ConstraintViolationListNormalizer.php index 86c5d088930..0b24fcb0fa0 100644 --- a/src/Hydra/Serializer/ConstraintViolationListNormalizer.php +++ b/src/Hydra/Serializer/ConstraintViolationListNormalizer.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Hydra\Serializer; -use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; use ApiPlatform\Serializer\AbstractConstraintViolationListNormalizer; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -24,9 +24,10 @@ */ final class ConstraintViolationListNormalizer extends AbstractConstraintViolationListNormalizer { + use HydraPrefixTrait; public const FORMAT = 'jsonld'; - public function __construct(private readonly UrlGeneratorInterface $urlGenerator, ?array $serializePayloadFields = null, ?NameConverterInterface $nameConverter = null) + public function __construct(?array $serializePayloadFields = null, ?NameConverterInterface $nameConverter = null) { parent::__construct($serializePayloadFields, $nameConverter); } @@ -36,19 +37,8 @@ public function __construct(private readonly UrlGeneratorInterface $urlGenerator */ public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null { - [$messages, $violations] = $this->getMessagesAndViolations($object); - - // TODO: in api platform 4 this will be the default, as right now we serialize a ValidationException instead of a ConstraintViolationList - if ($context['rfc_7807_compliant_errors'] ?? false) { - return $violations; - } - - return [ - '@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'ConstraintViolationList']), - '@type' => 'ConstraintViolationList', - 'hydra:title' => $context['title'] ?? 'An error occurred', - 'hydra:description' => $messages ? implode("\n", $messages) : (string) $object, - 'violations' => $violations, - ]; + [, $violations] = $this->getMessagesAndViolations($object); + + return $violations; } } diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php index 87f422d3723..b565fc4e0c6 100644 --- a/src/Hydra/Serializer/DocumentationNormalizer.php +++ b/src/Hydra/Serializer/DocumentationNormalizer.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Hydra\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; use ApiPlatform\Documentation\Documentation; +use ApiPlatform\JsonLd\ContextBuilder; use ApiPlatform\JsonLd\ContextBuilderInterface; +use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\CollectionOperationInterface; @@ -29,83 +29,100 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use ApiPlatform\Symfony\Validator\Exception\ValidationException; -use Symfony\Component\PropertyInfo\Type; +use ApiPlatform\Metadata\Util\TypeHelper; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\TypeIdentifier; + +use const ApiPlatform\JsonLd\HYDRA_CONTEXT; /** * Creates a machine readable Hydra API documentation. * * @author Kévin Dunglas */ -final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class DocumentationNormalizer implements NormalizerInterface { + use HydraPrefixTrait; public const FORMAT = 'jsonld'; - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator, private readonly ?NameConverterInterface $nameConverter = null) - { + public function __construct( + private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, + private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, + private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, + private readonly ResourceClassResolverInterface $resourceClassResolver, + private readonly UrlGeneratorInterface $urlGenerator, + private readonly ?NameConverterInterface $nameConverter = null, + private readonly ?array $defaultContext = [], + private readonly ?bool $entrypointEnabled = true, + ) { } /** * {@inheritdoc} */ - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $object, ?string $format = null, array $context = []): array { $classes = []; $entrypointProperties = []; + $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); foreach ($object->getResourceNameCollection() as $resourceClass) { $resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass); $resourceMetadata = $resourceMetadataCollection[0]; - if ($resourceMetadata instanceof ErrorResource && ValidationException::class === $resourceMetadata->getClass()) { + if (true === $resourceMetadata->getHideHydraOperation()) { continue; } $shortName = $resourceMetadata->getShortName(); $prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName"; - $this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $resourceMetadataCollection); - $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $resourceMetadataCollection); + + $this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $hydraPrefix, $resourceMetadataCollection); + $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix, $resourceMetadataCollection); } - return $this->computeDoc($object, $this->getClasses($entrypointProperties, $classes)); + return $this->computeDoc($object, $this->getClasses($entrypointProperties, $classes, $hydraPrefix), $hydraPrefix); } /** * Populates entrypoint properties. */ - private function populateEntrypointProperties(ApiResource $resourceMetadata, string $shortName, string $prefixedShortName, array &$entrypointProperties, ?ResourceMetadataCollection $resourceMetadataCollection = null): void + private function populateEntrypointProperties(ApiResource $resourceMetadata, string $shortName, string $prefixedShortName, array &$entrypointProperties, string $hydraPrefix, ?ResourceMetadataCollection $resourceMetadataCollection = null): void { - $hydraCollectionOperations = $this->getHydraOperations(true, $resourceMetadataCollection); + $hydraCollectionOperations = $this->getHydraOperations(true, $resourceMetadataCollection, $hydraPrefix); if (empty($hydraCollectionOperations)) { return; } $entrypointProperty = [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ + '@type' => $hydraPrefix.'SupportedProperty', + $hydraPrefix.'property' => [ '@id' => \sprintf('#Entrypoint/%s', lcfirst($shortName)), - '@type' => 'hydra:Link', + '@type' => $hydraPrefix.'Link', 'domain' => '#Entrypoint', - 'rdfs:label' => "The collection of $shortName resources", - 'rdfs:range' => [ - ['@id' => 'hydra:Collection'], + 'owl:maxCardinality' => 1, + 'range' => [ + ['@id' => $hydraPrefix.'Collection'], [ 'owl:equivalentClass' => [ - 'owl:onProperty' => ['@id' => 'hydra:member'], + 'owl:onProperty' => ['@id' => $hydraPrefix.'member'], 'owl:allValuesFrom' => ['@id' => $prefixedShortName], ], ], ], - 'hydra:supportedOperation' => $hydraCollectionOperations, + $hydraPrefix.'supportedOperation' => $hydraCollectionOperations, ], - 'hydra:title' => "The collection of $shortName resources", - 'hydra:readable' => true, - 'hydra:writeable' => false, + $hydraPrefix.'title' => "get{$shortName}Collection", + $hydraPrefix.'description' => "The collection of $shortName resources", + $hydraPrefix.'readable' => true, + $hydraPrefix.'writeable' => false, ]; if ($resourceMetadata->getDeprecationReason()) { @@ -118,22 +135,25 @@ private function populateEntrypointProperties(ApiResource $resourceMetadata, str /** * Gets a Hydra class. */ - private function getClass(string $resourceClass, ApiResource $resourceMetadata, string $shortName, string $prefixedShortName, array $context, ?ResourceMetadataCollection $resourceMetadataCollection = null): array + private function getClass(string $resourceClass, ApiResource $resourceMetadata, string $shortName, string $prefixedShortName, array $context, string $hydraPrefix, ?ResourceMetadataCollection $resourceMetadataCollection = null): array { $description = $resourceMetadata->getDescription(); $isDeprecated = $resourceMetadata->getDeprecationReason(); $class = [ '@id' => $prefixedShortName, - '@type' => 'hydra:Class', - 'rdfs:label' => $shortName, - 'hydra:title' => $shortName, - 'hydra:supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context), - 'hydra:supportedOperation' => $this->getHydraOperations(false, $resourceMetadataCollection), + '@type' => $hydraPrefix.'Class', + $hydraPrefix.'title' => $shortName, + $hydraPrefix.'supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix), + $hydraPrefix.'supportedOperation' => $this->getHydraOperations(false, $resourceMetadataCollection, $hydraPrefix), ]; if (null !== $description) { - $class['hydra:description'] = $description; + $class[$hydraPrefix.'description'] = $description; + } + + if ($resourceMetadata instanceof ErrorResource) { + $class['subClassOf'] = 'Error'; } if ($isDeprecated) { @@ -180,7 +200,7 @@ private function getPropertyMetadataFactoryContext(ApiResource $resourceMetadata /** * Gets Hydra properties. */ - private function getHydraProperties(string $resourceClass, ApiResource $resourceMetadata, string $shortName, string $prefixedShortName, array $context): array + private function getHydraProperties(string $resourceClass, ApiResource $resourceMetadata, string $shortName, string $prefixedShortName, array $context, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array { $classes = []; @@ -206,7 +226,6 @@ private function getHydraProperties(string $resourceClass, ApiResource $resource $classes = array_keys($classes); $properties = []; [$propertyNameContext, $propertyContext] = $this->getPropertyMetadataFactoryContext($resourceMetadata); - foreach ($classes as $class) { foreach ($this->propertyNameCollectionFactory->create($class, $propertyNameContext) as $propertyName) { $propertyMetadata = $this->propertyMetadataFactory->create($class, $propertyName, $propertyContext); @@ -219,7 +238,11 @@ private function getHydraProperties(string $resourceClass, ApiResource $resource $propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context); } - $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName); + if (false === $propertyMetadata->getHydra()) { + continue; + } + + $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName, $hydraPrefix); } } @@ -229,16 +252,20 @@ private function getHydraProperties(string $resourceClass, ApiResource $resource /** * Gets Hydra operations. */ - private function getHydraOperations(bool $collection, ?ResourceMetadataCollection $resourceMetadataCollection = null): array + private function getHydraOperations(bool $collection, ?ResourceMetadataCollection $resourceMetadataCollection = null, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array { $hydraOperations = []; foreach ($resourceMetadataCollection as $resourceMetadata) { foreach ($resourceMetadata->getOperations() as $operation) { + if (true === $operation->getHideHydraOperation()) { + continue; + } + if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) { continue; } - $hydraOperations[] = $this->getHydraOperation($operation, $operation->getTypes()[0] ?? "#{$operation->getShortName()}"); + $hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix); } } @@ -248,7 +275,7 @@ private function getHydraOperations(bool $collection, ?ResourceMetadataCollectio /** * Gets and populates if applicable a Hydra operation. */ - private function getHydraOperation(HttpOperation $operation, string $prefixedShortName): array + private function getHydraOperation(HttpOperation $operation, string $prefixedShortName, string $hydraPrefix): array { $method = $operation->getMethod() ?: 'GET'; @@ -266,50 +293,58 @@ private function getHydraOperation(HttpOperation $operation, string $prefixedSho if ('GET' === $method && $operation instanceof CollectionOperationInterface) { $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:FindAction'], - 'hydra:title' => "Retrieves the collection of $shortName resources.", - 'returns' => null === $outputClass ? 'owl:Nothing' : 'hydra:Collection', + '@type' => [$hydraPrefix.'Operation', 'schema:FindAction'], + $hydraPrefix.'description' => "Retrieves the collection of $shortName resources.", + 'returns' => null === $outputClass ? 'owl:Nothing' : $hydraPrefix.'Collection', ]; } elseif ('GET' === $method) { $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:FindAction'], - 'hydra:title' => "Retrieves a $shortName resource.", + '@type' => [$hydraPrefix.'Operation', 'schema:FindAction'], + $hydraPrefix.'description' => "Retrieves a $shortName resource.", 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, ]; } elseif ('PATCH' === $method) { $hydraOperation += [ - '@type' => 'hydra:Operation', - 'hydra:title' => "Updates the $shortName resource.", + '@type' => $hydraPrefix.'Operation', + $hydraPrefix.'description' => "Updates the $shortName resource.", 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, ]; + + if (null !== $inputClass) { + $possibleValue = []; + foreach ($operation->getInputFormats() ?? [] as $mimeTypes) { + foreach ($mimeTypes as $mimeType) { + $possibleValue[] = $mimeType; + } + } + + $hydraOperation['expectsHeader'] = [['headerName' => 'Content-Type', 'possibleValue' => $possibleValue]]; + } } elseif ('POST' === $method) { $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:CreateAction'], - 'hydra:title' => "Creates a $shortName resource.", + '@type' => [$hydraPrefix.'Operation', 'schema:CreateAction'], + $hydraPrefix.'description' => "Creates a $shortName resource.", 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, ]; } elseif ('PUT' === $method) { $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:ReplaceAction'], - 'hydra:title' => "Replaces the $shortName resource.", + '@type' => [$hydraPrefix.'Operation', 'schema:ReplaceAction'], + $hydraPrefix.'description' => "Replaces the $shortName resource.", 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, ]; } elseif ('DELETE' === $method) { $hydraOperation += [ - '@type' => ['hydra:Operation', 'schema:DeleteAction'], - 'hydra:title' => "Deletes the $shortName resource.", + '@type' => [$hydraPrefix.'Operation', 'schema:DeleteAction'], + $hydraPrefix.'description' => "Deletes the $shortName resource.", 'returns' => 'owl:Nothing', ]; } - $hydraOperation['hydra:method'] ?? $hydraOperation['hydra:method'] = $method; - - if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation['hydra:title'])) { - $hydraOperation['rdfs:label'] = $hydraOperation['hydra:title']; - } + $hydraOperation[$hydraPrefix.'method'] ??= $method; + $hydraOperation[$hydraPrefix.'title'] ??= strtolower($method).$shortName.($operation instanceof CollectionOperationInterface ? 'Collection' : ''); ksort($hydraOperation); @@ -327,61 +362,119 @@ private function getRange(ApiProperty $propertyMetadata): array|string|null return $jsonldContext['@type']; } - $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? []; $types = []; - foreach ($builtInTypes as $type) { - if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueTypes()[0] ?? null) { - $type = $collectionType; + if (method_exists(PropertyInfoExtractor::class, 'getType')) { + $nativeType = $propertyMetadata->getNativeType(); + if (null === $nativeType) { + return null; } - switch ($type->getBuiltinType()) { - case Type::BUILTIN_TYPE_STRING: - if (!\in_array('xmls:string', $types, true)) { - $types[] = 'xmls:string'; - } - break; - case Type::BUILTIN_TYPE_INT: - if (!\in_array('xmls:integer', $types, true)) { - $types[] = 'xmls:integer'; - } - break; - case Type::BUILTIN_TYPE_FLOAT: - if (!\in_array('xmls:decimal', $types, true)) { - $types[] = 'xmls:decimal'; - } - break; - case Type::BUILTIN_TYPE_BOOL: - if (!\in_array('xmls:boolean', $types, true)) { - $types[] = 'xmls:boolean'; - } - break; - case Type::BUILTIN_TYPE_OBJECT: - if (null === $className = $type->getClassName()) { - continue 2; + if ($nativeType->isSatisfiedBy(fn ($t) => $t instanceof CollectionType)) { + $nativeType = TypeHelper::getCollectionValueType($nativeType); + } + + // Check for specific types after potentially unwrapping the collection + if (null === $nativeType) { + return null; // Should not happen if collection had a value type, but safety check + } + + if ($nativeType->isIdentifiedBy(TypeIdentifier::STRING)) { + $types[] = 'xmls:string'; + } + + if ($nativeType->isIdentifiedBy(TypeIdentifier::INT)) { + $types[] = 'xmls:integer'; + } + + if ($nativeType->isIdentifiedBy(TypeIdentifier::FLOAT)) { + $types[] = 'xmls:decimal'; + } + + if ($nativeType->isIdentifiedBy(TypeIdentifier::BOOL)) { + $types[] = 'xmls:boolean'; + } + + if ($nativeType->isIdentifiedBy(\DateTimeInterface::class)) { + $types[] = 'xmls:dateTime'; + } + + /** @var class-string|null $className */ + $className = null; + + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); + }; + + if ($nativeType->isSatisfiedBy($typeIsResourceClass) && $className) { + $resourceMetadata = $this->resourceMetadataFactory->create($className); + $operation = $resourceMetadata->getOperation(); + + if (!$operation instanceof HttpOperation || !$operation->getTypes()) { + if (!\in_array("#{$operation->getShortName()}", $types, true)) { + $types[] = "#{$operation->getShortName()}"; } + } else { + $types = array_unique(array_merge($types, $operation->getTypes())); + } + } + // TODO: remove in 5.x + } else { + $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? []; - if (is_a($className, \DateTimeInterface::class, true)) { - if (!\in_array('xmls:dateTime', $types, true)) { - $types[] = 'xmls:dateTime'; + foreach ($builtInTypes as $type) { + if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueTypes()[0] ?? null) { + $type = $collectionType; + } + + switch ($type->getBuiltinType()) { + case LegacyType::BUILTIN_TYPE_STRING: + if (!\in_array('xmls:string', $types, true)) { + $types[] = 'xmls:string'; } break; - } - - if ($this->resourceClassResolver->isResourceClass($className)) { - $resourceMetadata = $this->resourceMetadataFactory->create($className); - $operation = $resourceMetadata->getOperation(); + case LegacyType::BUILTIN_TYPE_INT: + if (!\in_array('xmls:integer', $types, true)) { + $types[] = 'xmls:integer'; + } + break; + case LegacyType::BUILTIN_TYPE_FLOAT: + if (!\in_array('xmls:decimal', $types, true)) { + $types[] = 'xmls:decimal'; + } + break; + case LegacyType::BUILTIN_TYPE_BOOL: + if (!\in_array('xmls:boolean', $types, true)) { + $types[] = 'xmls:boolean'; + } + break; + case LegacyType::BUILTIN_TYPE_OBJECT: + if (null === $className = $type->getClassName()) { + continue 2; + } - if (!$operation instanceof HttpOperation || !$operation->getTypes()) { - if (!\in_array("#{$operation->getShortName()}", $types, true)) { - $types[] = "#{$operation->getShortName()}"; + if (is_a($className, \DateTimeInterface::class, true)) { + if (!\in_array('xmls:dateTime', $types, true)) { + $types[] = 'xmls:dateTime'; } break; } - $types = array_unique(array_merge($types, $operation->getTypes())); - break; - } + if ($this->resourceClassResolver->isResourceClass($className)) { + $resourceMetadata = $this->resourceMetadataFactory->create($className); + $operation = $resourceMetadata->getOperation(); + + if (!$operation instanceof HttpOperation || !$operation->getTypes()) { + if (!\in_array("#{$operation->getShortName()}", $types, true)) { + $types[] = "#{$operation->getShortName()}"; + } + break; + } + + $types = array_unique(array_merge($types, $operation->getTypes())); + break; + } + } } } @@ -389,16 +482,37 @@ private function getRange(ApiProperty $propertyMetadata): array|string|null return null; } + $types = array_unique($types); + return 1 === \count($types) ? $types[0] : $types; } private function isSingleRelation(ApiProperty $propertyMetadata): bool { + if (method_exists(PropertyInfoExtractor::class, 'getType')) { + $nativeType = $propertyMetadata->getNativeType(); + if (null === $nativeType) { + return false; + } + + if ($nativeType instanceof CollectionType) { + return false; + } + + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); + }; + + return $nativeType->isSatisfiedBy($typeIsResourceClass); + } + + // TODO: remove in 5.x $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? []; foreach ($builtInTypes as $type) { $className = $type->getClassName(); - if (!$type->isCollection() + if ( + !$type->isCollection() && null !== $className && $this->resourceClassResolver->isResourceClass($className) ) { @@ -412,78 +526,57 @@ private function isSingleRelation(ApiProperty $propertyMetadata): bool /** * Builds the classes array. */ - private function getClasses(array $entrypointProperties, array $classes): array + private function getClasses(array $entrypointProperties, array $classes, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array { - $classes[] = [ - '@id' => '#Entrypoint', - '@type' => 'hydra:Class', - 'hydra:title' => 'The API entrypoint', - 'hydra:supportedProperty' => $entrypointProperties, - 'hydra:supportedOperation' => [ - '@type' => 'hydra:Operation', - 'hydra:method' => 'GET', - 'rdfs:label' => 'The API entrypoint.', - 'returns' => '#EntryPoint', - ], - ]; + if ($this->entrypointEnabled) { + $classes[] = [ + '@id' => '#Entrypoint', + '@type' => $hydraPrefix.'Class', + $hydraPrefix.'title' => 'Entrypoint', + $hydraPrefix.'supportedProperty' => $entrypointProperties, + $hydraPrefix.'supportedOperation' => [ + '@type' => $hydraPrefix.'Operation', + $hydraPrefix.'method' => 'GET', + $hydraPrefix.'title' => 'index', + $hydraPrefix.'description' => 'The API Entrypoint.', + $hydraPrefix.'returns' => 'Entrypoint', + ], + ]; + } - // Constraint violation $classes[] = [ - '@id' => '#ConstraintViolation', - '@type' => 'hydra:Class', - 'hydra:title' => 'A constraint violation', - 'hydra:supportedProperty' => [ + '@id' => '#ConstraintViolationList', + '@type' => $hydraPrefix.'Class', + $hydraPrefix.'title' => 'ConstraintViolationList', + $hydraPrefix.'description' => 'A constraint violation List.', + $hydraPrefix.'supportedProperty' => [ [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#ConstraintViolation/propertyPath', + '@type' => $hydraPrefix.'SupportedProperty', + $hydraPrefix.'property' => [ + '@id' => '#ConstraintViolationList/propertyPath', '@type' => 'rdf:Property', 'rdfs:label' => 'propertyPath', - 'domain' => '#ConstraintViolation', + 'domain' => '#ConstraintViolationList', 'range' => 'xmls:string', ], - 'hydra:title' => 'propertyPath', - 'hydra:description' => 'The property path of the violation', - 'hydra:readable' => true, - 'hydra:writeable' => false, + $hydraPrefix.'title' => 'propertyPath', + $hydraPrefix.'description' => 'The property path of the violation', + $hydraPrefix.'readable' => true, + $hydraPrefix.'writeable' => false, ], [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#ConstraintViolation/message', + '@type' => $hydraPrefix.'SupportedProperty', + $hydraPrefix.'property' => [ + '@id' => '#ConstraintViolationList/message', '@type' => 'rdf:Property', 'rdfs:label' => 'message', - 'domain' => '#ConstraintViolation', - 'range' => 'xmls:string', - ], - 'hydra:title' => 'message', - 'hydra:description' => 'The message associated with the violation', - 'hydra:readable' => true, - 'hydra:writeable' => false, - ], - ], - ]; - - // Constraint violation list - $classes[] = [ - '@id' => '#ConstraintViolationList', - '@type' => 'hydra:Class', - 'subClassOf' => 'hydra:Error', - 'hydra:title' => 'A constraint violation list', - 'hydra:supportedProperty' => [ - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#ConstraintViolationList/violations', - '@type' => 'rdf:Property', - 'rdfs:label' => 'violations', 'domain' => '#ConstraintViolationList', - 'range' => '#ConstraintViolation', + 'range' => 'xmls:string', ], - 'hydra:title' => 'violations', - 'hydra:description' => 'The violations', - 'hydra:readable' => true, - 'hydra:writeable' => false, + $hydraPrefix.'title' => 'message', + $hydraPrefix.'description' => 'The message associated with the violation', + $hydraPrefix.'readable' => true, + $hydraPrefix.'writeable' => false, ], ], ]; @@ -494,7 +587,7 @@ private function getClasses(array $entrypointProperties, array $classes): array /** * Gets a property definition. */ - private function getProperty(ApiProperty $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName): array + private function getProperty(ApiProperty $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName, string $hydraPrefix): array { if ($iri = $propertyMetadata->getIris()) { $iri = 1 === (is_countable($iri) ? \count($iri) : 0) ? $iri[0] : $iri; @@ -504,11 +597,11 @@ private function getProperty(ApiProperty $propertyMetadata, string $propertyName $iri = "#$shortName/$propertyName"; } - $propertyData = ($propertyMetadata->getJsonldContext()['hydra:property'] ?? []) + [ + $propertyData = ($propertyMetadata->getJsonldContext()[$hydraPrefix.'property'] ?? []) + [ '@id' => $iri, - '@type' => false === $propertyMetadata->isReadableLink() ? 'hydra:Link' : 'rdf:Property', - 'rdfs:label' => $propertyName, + '@type' => false === $propertyMetadata->isReadableLink() ? $hydraPrefix.'Link' : 'rdf:Property', 'domain' => $prefixedShortName, + 'label' => $propertyName, ]; if (!isset($propertyData['owl:deprecated']) && $propertyMetadata->getDeprecationReason()) { @@ -524,16 +617,16 @@ private function getProperty(ApiProperty $propertyMetadata, string $propertyName } $property = [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => $propertyData, - 'hydra:title' => $propertyName, - 'hydra:required' => $propertyMetadata->isRequired(), - 'hydra:readable' => $propertyMetadata->isReadable(), - 'hydra:writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(), + '@type' => $hydraPrefix.'SupportedProperty', + $hydraPrefix.'property' => $propertyData, + $hydraPrefix.'title' => $propertyName, + $hydraPrefix.'required' => $propertyMetadata->isRequired() ?? false, + $hydraPrefix.'readable' => $propertyMetadata->isReadable(), + $hydraPrefix.'writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(), ]; if (null !== $description = $propertyMetadata->getDescription()) { - $property['hydra:description'] = $description; + $property[$hydraPrefix.'description'] = $description; } return $property; @@ -542,20 +635,22 @@ private function getProperty(ApiProperty $propertyMetadata, string $propertyName /** * Computes the documentation. */ - private function computeDoc(Documentation $object, array $classes): array + private function computeDoc(Documentation $object, array $classes, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array { - $doc = ['@context' => $this->getContext(), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => 'hydra:ApiDocumentation']; + $doc = ['@context' => $this->getContext($hydraPrefix), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => $hydraPrefix.'ApiDocumentation']; if ('' !== $object->getTitle()) { - $doc['hydra:title'] = $object->getTitle(); + $doc[$hydraPrefix.'title'] = $object->getTitle(); } if ('' !== $object->getDescription()) { - $doc['hydra:description'] = $object->getDescription(); + $doc[$hydraPrefix.'description'] = $object->getDescription(); } - $doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint'); - $doc['hydra:supportedClass'] = $classes; + if ($this->entrypointEnabled) { + $doc[$hydraPrefix.'entrypoint'] = $this->urlGenerator->generate('api_entrypoint'); + } + $doc[$hydraPrefix.'supportedClass'] = $classes; return $doc; } @@ -563,21 +658,22 @@ private function computeDoc(Documentation $object, array $classes): array /** * Builds the JSON-LD context for the API documentation. */ - private function getContext(): array + private function getContext(string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array { return [ - '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#', - 'hydra' => ContextBuilderInterface::HYDRA_NS, - 'rdf' => ContextBuilderInterface::RDF_NS, - 'rdfs' => ContextBuilderInterface::RDFS_NS, - 'xmls' => ContextBuilderInterface::XML_NS, - 'owl' => ContextBuilderInterface::OWL_NS, - 'schema' => ContextBuilderInterface::SCHEMA_ORG_NS, - 'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'], - 'range' => ['@id' => 'rdfs:range', '@type' => '@id'], - 'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'], - 'expects' => ['@id' => 'hydra:expects', '@type' => '@id'], - 'returns' => ['@id' => 'hydra:returns', '@type' => '@id'], + HYDRA_CONTEXT, + [ + '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#', + 'hydra' => ContextBuilderInterface::HYDRA_NS, + 'rdf' => ContextBuilderInterface::RDF_NS, + 'rdfs' => ContextBuilderInterface::RDFS_NS, + 'xmls' => ContextBuilderInterface::XML_NS, + 'owl' => ContextBuilderInterface::OWL_NS, + 'schema' => ContextBuilderInterface::SCHEMA_ORG_NS, + 'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'], + 'range' => ['@id' => 'rdfs:range', '@type' => '@id'], + 'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'], + ], ]; } @@ -589,22 +685,11 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && $data instanceof Documentation; } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { return self::FORMAT === $format ? [Documentation::class => true] : []; } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } } diff --git a/src/Hydra/Serializer/EntrypointNormalizer.php b/src/Hydra/Serializer/EntrypointNormalizer.php index 669099a33f6..77dda8539eb 100644 --- a/src/Hydra/Serializer/EntrypointNormalizer.php +++ b/src/Hydra/Serializer/EntrypointNormalizer.php @@ -13,30 +13,25 @@ namespace ApiPlatform\Hydra\Serializer; -use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; -use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Exception\OperationNotFoundException; +use ApiPlatform\Documentation\Entrypoint; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\OperationNotFoundException; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Normalizes the API entrypoint. * * @author Kévin Dunglas */ -final class EntrypointNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class EntrypointNormalizer implements NormalizerInterface { public const FORMAT = 'jsonld'; - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator) + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface $iriConverter, private readonly UrlGeneratorInterface $urlGenerator) { } @@ -61,12 +56,12 @@ public function normalize(mixed $object, ?string $format = null, array $context foreach ($resource->getOperations() as $operation) { $key = lcfirst($resource->getShortName()); - if (!$operation instanceof CollectionOperationInterface || isset($entrypoint[$key])) { + if (true === $operation->getHideHydraOperation() || !$operation instanceof CollectionOperationInterface || isset($entrypoint[$key])) { continue; } try { - $entrypoint[$key] = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $operation); // @phpstan-ignore-line phpstan issue as type is CollectionOperationInterface & Operation + $entrypoint[$key] = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $operation); } catch (InvalidArgumentException|OperationNotFoundException) { // Ignore resources without GET operations } @@ -84,25 +79,14 @@ public function normalize(mixed $object, ?string $format = null, array $context */ public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { - return self::FORMAT === $format && ($data instanceof Entrypoint || $data instanceof DocumentationEntrypoint); + return self::FORMAT === $format && $data instanceof Entrypoint; } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { - return self::FORMAT === $format ? [Entrypoint::class => true, DocumentationEntrypoint::class => true] : []; - } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; + return self::FORMAT === $format ? [Entrypoint::class => true] : []; } } diff --git a/src/Hydra/Serializer/ErrorNormalizer.php b/src/Hydra/Serializer/ErrorNormalizer.php deleted file mode 100644 index fa8c9634a58..00000000000 --- a/src/Hydra/Serializer/ErrorNormalizer.php +++ /dev/null @@ -1,101 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Hydra\Serializer; - -use ApiPlatform\Api\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use ApiPlatform\State\ApiResource\Error; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; - -/** - * Converts {@see \Exception} or {@see FlattenException} to a Hydra error representation. - * - * @deprecated Errors are resources since API Platform 3.2 we use the ItemNormalizer - * - * @author Kévin Dunglas - * @author Samuel ROZE - */ -final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface -{ - use ErrorNormalizerTrait; - - public const FORMAT = 'jsonld'; - public const TITLE = 'title'; - private array $defaultContext = [self::TITLE => 'An error occurred']; - - public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly bool $debug = false, array $defaultContext = []) - { - $this->defaultContext = array_merge($this->defaultContext, $defaultContext); - } - - /** - * {@inheritdoc} - */ - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null - { - $data = [ - '@context' => $this->urlGenerator->generate('api_jsonld_context', ['shortName' => 'Error']), - '@type' => 'hydra:Error', - 'hydra:title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE], - 'hydra:description' => $this->getErrorMessage($object, $context, $this->debug), - ]; - - if ($this->debug && null !== $trace = $object->getTrace()) { - $data['trace'] = $trace; - } - - return $data; - } - - /** - * {@inheritdoc} - */ - public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool - { - if ($context['api_error_resource'] ?? false) { - return false; - } - - return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException); - } - - public function getSupportedTypes($format): array - { - if (self::FORMAT === $format) { - return [ - \Exception::class => true, - Error::class => false, - FlattenException::class => true, - ]; - } - - return []; - } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } -} diff --git a/src/Hydra/Serializer/ErrorNormalizerTrait.php b/src/Hydra/Serializer/ErrorNormalizerTrait.php deleted file mode 100644 index 12fec8ff0fc..00000000000 --- a/src/Hydra/Serializer/ErrorNormalizerTrait.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\Hydra\Serializer; - -use ApiPlatform\Exception\ErrorCodeSerializableInterface; -use Symfony\Component\ErrorHandler\Exception\FlattenException; -use Symfony\Component\HttpFoundation\Response; - -/** - * @internal - */ -trait ErrorNormalizerTrait -{ - private function getErrorMessage($object, array $context, bool $debug = false): string - { - $message = $object->getMessage(); - - if ($debug) { - return $message; - } - - if ($object instanceof FlattenException) { - $statusCode = $context['statusCode'] ?? $object->getStatusCode(); - if ($statusCode >= 500 && $statusCode < 600) { - $message = Response::$statusTexts[$statusCode] ?? Response::$statusTexts[Response::HTTP_INTERNAL_SERVER_ERROR]; - } - } - - return $message; - } - - private function getErrorCode(object $object): ?string - { - if ($object instanceof FlattenException) { - $exceptionClass = $object->getClass(); - } else { - $exceptionClass = $object::class; - } - - if (is_a($exceptionClass, ErrorCodeSerializableInterface::class, true)) { - return $exceptionClass::getErrorCode(); - } - - return null; - } -} diff --git a/src/Hydra/Serializer/HydraPrefixNameConverter.php b/src/Hydra/Serializer/HydraPrefixNameConverter.php new file mode 100644 index 00000000000..370255bde89 --- /dev/null +++ b/src/Hydra/Serializer/HydraPrefixNameConverter.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Serializer; + +use ApiPlatform\JsonLd\ContextBuilder; +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +final readonly class HydraPrefixNameConverter implements NameConverterInterface, AdvancedNameConverterInterface +{ + /** + * @param array $defaultContext + */ + public function __construct(private NameConverterInterface $nameConverter, private array $defaultContext = []) + { + } + + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + $context += $this->defaultContext; + $name = $this->nameConverter->normalize($propertyName, $class, $format, $context); + + if (true === ($context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX] ?? true)) { + return $name; + } + + return str_starts_with($name, ContextBuilder::HYDRA_PREFIX) ? str_replace(ContextBuilder::HYDRA_PREFIX, '', $name) : $name; + } + + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + return $this->nameConverter->denormalize($propertyName, $class, $format, $context); + } +} diff --git a/src/Hydra/Serializer/PartialCollectionViewNormalizer.php b/src/Hydra/Serializer/PartialCollectionViewNormalizer.php index 95fb34245d2..c37586f7e95 100644 --- a/src/Hydra/Serializer/PartialCollectionViewNormalizer.php +++ b/src/Hydra/Serializer/PartialCollectionViewNormalizer.php @@ -13,20 +13,19 @@ namespace ApiPlatform\Hydra\Serializer; +use ApiPlatform\Hydra\State\Util\PaginationHelperTrait; +use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; +use ApiPlatform\Metadata\Util\IriHelper; use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use ApiPlatform\Util\IriHelper; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Serializer\Exception\UnexpectedValueException; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Adds a view key to the result of a paginated Hydra collection. @@ -34,11 +33,16 @@ * @author Kévin Dunglas * @author Samuel ROZE */ -final class PartialCollectionViewNormalizer implements NormalizerInterface, NormalizerAwareInterface, CacheableSupportsMethodInterface +final class PartialCollectionViewNormalizer implements NormalizerInterface, NormalizerAwareInterface { + use HydraPrefixTrait; + use PaginationHelperTrait; private readonly PropertyAccessorInterface $propertyAccessor; - public function __construct(private readonly NormalizerInterface $collectionNormalizer, private readonly string $pageParameterName = 'page', private string $enabledParameterName = 'pagination', private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, ?PropertyAccessorInterface $propertyAccessor = null, private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH) + /** + * @param array $defaultContext + */ + public function __construct(private readonly NormalizerInterface $collectionNormalizer, private readonly string $pageParameterName = 'page', private string $enabledParameterName = 'pagination', private readonly ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, ?PropertyAccessorInterface $propertyAccessor = null, private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH, private readonly array $defaultContext = []) { $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); } @@ -58,21 +62,11 @@ public function normalize(mixed $object, ?string $format = null, array $context throw new UnexpectedValueException('Expected data to be an array'); } - $currentPage = $lastPage = $itemsPerPage = $pageTotalItems = null; - if ($paginated = ($object instanceof PartialPaginatorInterface)) { - if ($object instanceof PaginatorInterface) { - $paginated = 1. !== $lastPage = $object->getLastPage(); - } else { - $itemsPerPage = $object->getItemsPerPage(); - $pageTotalItems = (float) \count($object); - } - - $currentPage = $object->getCurrentPage(); + $paginated = $object instanceof PartialPaginatorInterface; + if ($paginated && $object instanceof PaginatorInterface) { + $paginated = 1. !== $object->getLastPage(); } - // TODO: This needs to be changed as well as I wrote in the CollectionFiltersNormalizer - // We should not rely on the request_uri but instead rely on the UriTemplate - // This needs that we implement the RFC and that we do more parsing before calling the serialization (MainController) $parsed = IriHelper::parseIri($context['uri'] ?? $context['request_uri'] ?? '/', $this->pageParameterName); $appliedFilters = $parsed['parameters']; unset($appliedFilters[$this->enabledParameterName]); @@ -91,18 +85,36 @@ public function normalize(mixed $object, ?string $format = null, array $context $cursorPaginationAttribute = $operation instanceof HttpOperation ? $operation->getPaginationViaCursor() : null; $isPaginatedWithCursor = (bool) $cursorPaginationAttribute; - $data['hydra:view'] = ['@id' => null, '@type' => 'hydra:PartialCollectionView']; + $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); if ($isPaginatedWithCursor) { - return $this->populateDataWithCursorBasedPagination($data, $parsed, $object, $cursorPaginationAttribute, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy); + $data[$hydraPrefix.'view'] = ['@id' => null, '@type' => $hydraPrefix.'PartialCollectionView']; + + return $this->populateDataWithCursorBasedPagination($data, $parsed, $object, $cursorPaginationAttribute, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy, $hydraPrefix); + } + + $partialCollectionView = $this->getPartialCollectionView($object, $context['uri'] ?? $context['request_uri'] ?? '/', $this->pageParameterName, $this->enabledParameterName, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy); + + $view = [ + '@id' => $partialCollectionView->id, + '@type' => $hydraPrefix.'PartialCollectionView', + ]; + + if (null !== $partialCollectionView->first) { + $view[$hydraPrefix.'first'] = $partialCollectionView->first; + $view[$hydraPrefix.'last'] = $partialCollectionView->last; } - $data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $paginated ? $currentPage : null, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy); + if (null !== $partialCollectionView->previous) { + $view[$hydraPrefix.'previous'] = $partialCollectionView->previous; + } - if ($paginated) { - return $this->populateDataWithPagination($data, $parsed, $currentPage, $lastPage, $itemsPerPage, $pageTotalItems, $operation?->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy); + if (null !== $partialCollectionView->next) { + $view[$hydraPrefix.'next'] = $partialCollectionView->next; } + $data[$hydraPrefix.'view'] = $view; + return $data; } @@ -114,32 +126,14 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return $this->collectionNormalizer->supportsNormalization($data, $format, $context); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->collectionNormalizer, 'getSupportedTypes')) { - return [ - '*' => $this->collectionNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod(), - ]; - } - return $this->collectionNormalizer->getSupportedTypes($format); } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->collectionNormalizer instanceof BaseCacheableSupportsMethodInterface && $this->collectionNormalizer->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} */ @@ -150,6 +144,9 @@ public function setNormalizer(NormalizerInterface $normalizer): void } } + /** + * @param object $object + */ private function cursorPaginationFields(array $fields, int $direction, $object): array { $paginationFilters = []; @@ -168,38 +165,20 @@ private function cursorPaginationFields(array $fields, int $direction, $object): return $paginationFilters; } - private function populateDataWithCursorBasedPagination(array $data, array $parsed, \Traversable $object, ?array $cursorPaginationAttribute, ?int $urlGenerationStrategy): array + private function populateDataWithCursorBasedPagination(array $data, array $parsed, \Traversable $object, ?array $cursorPaginationAttribute, ?int $urlGenerationStrategy, string $hydraPrefix): array { $objects = iterator_to_array($object); $firstObject = current($objects); $lastObject = end($objects); - $data['hydra:view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], urlGenerationStrategy: $urlGenerationStrategy); + $data[$hydraPrefix.'view']['@id'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], urlGenerationStrategy: $urlGenerationStrategy); if (false !== $lastObject && \is_array($cursorPaginationAttribute)) { - $data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, 1, $lastObject)), urlGenerationStrategy: $urlGenerationStrategy); + $data[$hydraPrefix.'view'][$hydraPrefix.'next'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, 1, $lastObject)), urlGenerationStrategy: $urlGenerationStrategy); } if (false !== $firstObject && \is_array($cursorPaginationAttribute)) { - $data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, -1, $firstObject)), urlGenerationStrategy: $urlGenerationStrategy); - } - - return $data; - } - - private function populateDataWithPagination(array $data, array $parsed, ?float $currentPage, ?float $lastPage, ?float $itemsPerPage, ?float $pageTotalItems, ?int $urlGenerationStrategy): array - { - if (null !== $lastPage) { - $data['hydra:view']['hydra:first'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, 1., $urlGenerationStrategy); - $data['hydra:view']['hydra:last'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $lastPage, $urlGenerationStrategy); - } - - if (1. !== $currentPage) { - $data['hydra:view']['hydra:previous'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage - 1., $urlGenerationStrategy); - } - - if ((null !== $lastPage && $currentPage < $lastPage) || (null === $lastPage && $pageTotalItems >= $itemsPerPage)) { - $data['hydra:view']['hydra:next'] = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $this->pageParameterName, $currentPage + 1., $urlGenerationStrategy); + $data[$hydraPrefix.'view'][$hydraPrefix.'previous'] = IriHelper::createIri($parsed['parts'], array_merge($parsed['parameters'], $this->cursorPaginationFields($cursorPaginationAttribute, -1, $firstObject)), urlGenerationStrategy: $urlGenerationStrategy); } return $data; diff --git a/src/Hydra/State/JsonStreamerProcessor.php b/src/Hydra/State/JsonStreamerProcessor.php new file mode 100644 index 00000000000..967316ccd8f --- /dev/null +++ b/src/Hydra/State/JsonStreamerProcessor.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\State; + +use ApiPlatform\Hydra\Collection; +use ApiPlatform\Hydra\State\Util\PaginationHelperTrait; +use ApiPlatform\Hydra\State\Util\SearchHelperTrait; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Error; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\State\Pagination\PaginatorInterface; +use ApiPlatform\State\Pagination\PartialPaginatorInterface; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\Util\HttpResponseHeadersTrait; +use ApiPlatform\State\Util\HttpResponseStatusTrait; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\JsonStreamer\StreamWriterInterface; +use Symfony\Component\TypeInfo\Type; + +/** + * @implements ProcessorInterface + */ +final class JsonStreamerProcessor implements ProcessorInterface +{ + use HttpResponseHeadersTrait; + use HttpResponseStatusTrait; + use PaginationHelperTrait; + use SearchHelperTrait; + + /** + * @param ProcessorInterface|null $processor + * @param StreamWriterInterface> $jsonStreamer + */ + public function __construct( + private readonly ?ProcessorInterface $processor, + private readonly StreamWriterInterface $jsonStreamer, + ?IriConverterInterface $iriConverter = null, + ?ResourceClassResolverInterface $resourceClassResolver = null, + ?OperationMetadataFactoryInterface $operationMetadataFactory = null, + private readonly string $pageParameterName = 'page', + private readonly string $enabledParameterName = 'pagination', + private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH, + ) { + $this->resourceClassResolver = $resourceClassResolver; + $this->iriConverter = $iriConverter; + $this->operationMetadataFactory = $operationMetadataFactory; + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) + { + if ( + $operation instanceof Error + || $data instanceof Response + || !$operation instanceof HttpOperation + || !($request = $context['request'] ?? null) + || !$operation->getJsonStream() + || 'jsonld' !== $request->getRequestFormat() + ) { + return $this->processor?->process($data, $operation, $uriVariables, $context); + } + + if ($operation instanceof CollectionOperationInterface) { + $requestUri = $request->getRequestUri() ?? ''; + $collection = new Collection(); + $collection->member = $data; + $collection->view = $this->getPartialCollectionView($data, $requestUri, $this->pageParameterName, $this->enabledParameterName, $operation->getUrlGenerationStrategy() ?? $this->urlGenerationStrategy); + + if ($operation->getParameters()) { + $parts = parse_url(https://github.com/$requestUri); + $collection->search = $this->getSearch($parts['path'] ?? '', $operation); + } + + if ($data instanceof PaginatorInterface) { + $collection->totalItems = $data->getTotalItems(); + } + + if (\is_array($data) || ($data instanceof \Countable && !$data instanceof PartialPaginatorInterface)) { + $collection->totalItems = \count($data); + } + + $data = $this->jsonStreamer->write( + $collection, + Type::generic(Type::object($collection::class), Type::object($operation->getClass())), + ['data' => $data, 'operation' => $operation], + ); + } else { + $data = $this->jsonStreamer->write( + $data, + Type::object($operation->getClass()), + ['data' => $data, 'operation' => $operation], + ); + } + + /** @var iterable $data */ + $response = new StreamedResponse( + $data, + $this->getStatus($request, $operation, $context), + $this->getHeaders($request, $operation, $context) + ); + + return $this->processor ? $this->processor->process($response, $operation, $uriVariables, $context) : $response; + } +} diff --git a/src/Hydra/State/JsonStreamerProvider.php b/src/Hydra/State/JsonStreamerProvider.php new file mode 100644 index 00000000000..8e4e143baa9 --- /dev/null +++ b/src/Hydra/State/JsonStreamerProvider.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\State; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use Symfony\Component\JsonStreamer\StreamReaderInterface; +use Symfony\Component\TypeInfo\Type; + +final class JsonStreamerProvider implements ProviderInterface +{ + public function __construct( + private readonly ?ProviderInterface $decorated, + private readonly StreamReaderInterface $jsonStreamReader, + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + if (!$operation instanceof HttpOperation || !$operation->getJsonStream() || !($request = $context['request'] ?? null)) { + return $this->decorated?->provide($operation, $uriVariables, $context); + } + + $data = $this->decorated ? $this->decorated->provide($operation, $uriVariables, $context) : $request->attributes->get('data'); + + if (!$operation->canDeserialize() || 'jsonld' !== $request->attributes->get('input_format')) { + return $data; + } + + $data = $this->jsonStreamReader->read($request->getContent(true), Type::object($operation->getClass())); + $context['request']->attributes->set('deserialized', true); + + return $data; + } +} diff --git a/src/Hydra/State/Util/PaginationHelperTrait.php b/src/Hydra/State/Util/PaginationHelperTrait.php new file mode 100644 index 00000000000..82c30651ac6 --- /dev/null +++ b/src/Hydra/State/Util/PaginationHelperTrait.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\State\Util; + +use ApiPlatform\Hydra\PartialCollectionView; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Metadata\Util\IriHelper; +use ApiPlatform\State\Pagination\PaginatorInterface; +use ApiPlatform\State\Pagination\PartialPaginatorInterface; + +trait PaginationHelperTrait +{ + private function getPaginationIri(array $parsed, ?float $currentPage, ?float $lastPage, ?float $itemsPerPage, ?float $pageTotalItems, ?int $urlGenerationStrategy, string $pageParameterName): array + { + $first = $last = $previous = $next = null; + + if (null !== $lastPage) { + $first = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $pageParameterName, 1., $urlGenerationStrategy); + $last = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $pageParameterName, $lastPage, $urlGenerationStrategy); + } + + if (1. !== $currentPage) { + $previous = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $pageParameterName, $currentPage - 1., $urlGenerationStrategy); + } + + if ((null !== $lastPage && $currentPage < $lastPage) || (null === $lastPage && $pageTotalItems >= $itemsPerPage)) { + $next = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $pageParameterName, $currentPage + 1., $urlGenerationStrategy); + } + + return [ + 'first' => $first, + 'last' => $last, + 'previous' => $previous, + 'next' => $next, + ]; + } + + private function getPartialCollectionView(mixed $object, string $requestUri, string $pageParameterName, string $enabledParameterName, ?int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH): PartialCollectionView + { + $currentPage = $lastPage = $itemsPerPage = $pageTotalItems = null; + $paginated = false; + if ($object instanceof PartialPaginatorInterface) { + $paginated = true; + if ($object instanceof PaginatorInterface) { + $paginated = 1. !== $lastPage = $object->getLastPage(); + } else { + $itemsPerPage = $object->getItemsPerPage(); + $pageTotalItems = (float) \count($object); + } + $currentPage = $object->getCurrentPage(); + } + + $parsed = IriHelper::parseIri($requestUri, $pageParameterName); + $appliedFilters = $parsed['parameters']; + unset($appliedFilters[$enabledParameterName]); + + $id = IriHelper::createIri($parsed['parts'], $parsed['parameters'], $pageParameterName, $paginated ? $currentPage : null, $urlGenerationStrategy); + + if (!$paginated && $appliedFilters) { + return new PartialCollectionView($id); + } + + ['first' => $first, 'last' => $last, 'previous' => $previous, 'next' => $next] = $this->getPaginationIri($parsed, $currentPage, $lastPage, $itemsPerPage, $pageTotalItems, $urlGenerationStrategy, $pageParameterName); + + if (!$paginated) { + $first = null; + $last = null; + } + + return new PartialCollectionView( + $id, + $first, + $last, + $previous, + $next + ); + } +} diff --git a/src/Hydra/State/Util/SearchHelperTrait.php b/src/Hydra/State/Util/SearchHelperTrait.php new file mode 100644 index 00000000000..30c203f5621 --- /dev/null +++ b/src/Hydra/State/Util/SearchHelperTrait.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\State\Util; + +use ApiPlatform\Hydra\IriTemplate; +use ApiPlatform\Hydra\IriTemplateMapping; +use ApiPlatform\Metadata\FilterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameters; +use ApiPlatform\Metadata\QueryParameterInterface; + +trait SearchHelperTrait +{ + /** + * @param FilterInterface[] $filters + */ + private function getSearch(string $path, ?Operation $operation = null, ?string $resourceClass = null, ?array $filters = [], ?Parameters $parameters = null, ?callable $getFilter = null): IriTemplate + { + ['mapping' => $mapping, 'keys' => $keys] = $this->getSearchMappingAndKeys($operation, $resourceClass, $filters, $parameters, $getFilter); + + return new IriTemplate( + variableRepresentation: 'BasicRepresentation', + mapping: $mapping, + template: \sprintf('%s{?%s}', $path, implode(',', $keys)), + ); + } + + /** + * @param FilterInterface[] $filters + * + * @return array{mapping: list, keys: list} + */ + private function getSearchMappingAndKeys(?Operation $operation = null, ?string $resourceClass = null, ?array $filters = [], ?Parameters $parameters = null, ?callable $getFilter = null): array + { + $mapping = []; + $keys = []; + + if ($filters) { + foreach ($filters as $filter) { + foreach ($filter->getDescription($resourceClass) as $variable => $data) { + $keys[] = $variable; + $mapping[] = new IriTemplateMapping(variable: $variable, property: $data['property'] ?? null, required: $data['required'] ?? false); + } + } + } + + $params = $operation ? ($operation->getParameters() ?? []) : ($parameters ?? []); + + foreach ($params as $key => $parameter) { + if (!$parameter instanceof QueryParameterInterface || false === $parameter->getHydra()) { + continue; + } + + if ($getFilter && ($filterId = $parameter->getFilter()) && \is_string($filterId) && ($filter = $getFilter($filterId))) { + $filterDescription = $filter->getDescription($resourceClass); + + foreach ($filterDescription as $variable => $description) { + // // This is a practice induced by PHP and is not necessary when implementing URI template + if (str_ends_with((string) $variable, '[]')) { + continue; + } + + if (!($descriptionProperty = $description['property'] ?? null)) { + continue; + } + + if (($prop = $parameter->getProperty()) && $descriptionProperty !== $prop) { + continue; + } + + $k = str_replace(':property', $description['property'], $key); + $variable = str_replace($description['property'], $k, $variable); + $keys[] = $variable; + $m = new IriTemplateMapping(variable: $variable, property: $description['property'], required: $description['required']); + if (null !== ($required = $parameter->getRequired())) { + $m->required = $required; + } + $mapping[] = $m; + } + + if ($filterDescription) { + continue; + } + } + + if (str_contains($key, ':property') && $parameter->getProperties()) { + $required = $parameter->getRequired(); + foreach ($parameter->getProperties() as $prop) { + $k = str_replace(':property', $prop, $key); + $m = new IriTemplateMapping(variable: $k, property: $prop); + $keys[] = $k; + if (null !== $required) { + $m->required = $required; + } + $mapping[] = $m; + } + + continue; + } + + if (!($property = $parameter->getProperty())) { + continue; + } + + $m = new IriTemplateMapping(variable: $key, property: $property); + $keys[] = $key; + if (null !== ($required = $parameter->getRequired())) { + $m->required = $required; + } + $mapping[] = $m; + } + + return ['mapping' => $mapping, 'keys' => $keys]; + } +} diff --git a/src/Hydra/Tests/Fixtures/CustomConverter.php b/src/Hydra/Tests/Fixtures/CustomConverter.php new file mode 100644 index 00000000000..969f65c8a2e --- /dev/null +++ b/src/Hydra/Tests/Fixtures/CustomConverter.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Fixtures; + +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +/** + * Custom converter that will only convert a property named "nameConverted" + * with the same logic as Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter. + */ +class CustomConverter implements NameConverterInterface +{ + private NameConverterInterface $nameConverter; + + public function __construct() + { + $this->nameConverter = new CamelCaseToSnakeCaseNameConverter(); + } + + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + return 'nameConverted' === $propertyName ? $this->nameConverter->normalize($propertyName) : $propertyName; + } + + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + return 'name_converted' === $propertyName ? $this->nameConverter->denormalize($propertyName) : $propertyName; + } +} diff --git a/src/Hydra/Tests/Fixtures/Dummy.php b/src/Hydra/Tests/Fixtures/Dummy.php new file mode 100644 index 00000000000..bf355e87577 --- /dev/null +++ b/src/Hydra/Tests/Fixtures/Dummy.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Fixtures; + +class Dummy +{ +} diff --git a/src/Hydra/Tests/Fixtures/Entity/SoMany.php b/src/Hydra/Tests/Fixtures/Entity/SoMany.php new file mode 100644 index 00000000000..493928369fb --- /dev/null +++ b/src/Hydra/Tests/Fixtures/Entity/SoMany.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Fixtures\Entity; + +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +#[ApiResource(paginationPartial: true, paginationViaCursor: [['field' => 'id', 'direction' => 'DESC']])] +#[ORM\Entity] +class SoMany +{ + public $id; + public $content; +} diff --git a/src/Hydra/Tests/Fixtures/Foo.php b/src/Hydra/Tests/Fixtures/Foo.php new file mode 100644 index 00000000000..e720d223926 --- /dev/null +++ b/src/Hydra/Tests/Fixtures/Foo.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Fixtures; + +class Foo +{ + public int $id; + public string $bar; +} diff --git a/src/Hydra/Tests/Fixtures/NotAResource.php b/src/Hydra/Tests/Fixtures/NotAResource.php new file mode 100644 index 00000000000..4906cf53fb1 --- /dev/null +++ b/src/Hydra/Tests/Fixtures/NotAResource.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * This class is not mapped as an API resource. + * + * @author Kévin Dunglas + */ +class NotAResource +{ + public function __construct( + #[Groups('contain_non_resource')] + private $foo, + #[Groups('contain_non_resource')] + private $bar, + ) { + } + + public function getFoo() + { + return $this->foo; + } + + public function getBar() + { + return $this->bar; + } +} diff --git a/src/Hydra/Tests/JsonSchema/SchemaFactoryTest.php b/src/Hydra/Tests/JsonSchema/SchemaFactoryTest.php new file mode 100644 index 00000000000..25f1a2ffda7 --- /dev/null +++ b/src/Hydra/Tests/JsonSchema/SchemaFactoryTest.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\JsonSchema; + +use ApiPlatform\Hydra\JsonSchema\SchemaFactory; +use ApiPlatform\Hydra\Tests\Fixtures\Dummy; +use ApiPlatform\JsonLd\ContextBuilder; +use ApiPlatform\JsonSchema\DefinitionNameFactory; +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; + +class SchemaFactoryTest extends TestCase +{ + use ProphecyTrait; + + private SchemaFactory $schemaFactory; + + protected function setUp(): void + { + $resourceMetadataFactoryCollection = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryCollection->create(Dummy::class)->willReturn( + new ResourceMetadataCollection(Dummy::class, [ + new ApiResource(operations: [ + 'get' => new Get(name: 'get'), + ]), + ]) + ); + + $propertyNameCollectionFactory = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true, 'schema_type' => Schema::TYPE_OUTPUT])->willReturn(new PropertyNameCollection(['id', 'name'])); + $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactory->create(Dummy::class, 'id', Argument::type('array'))->willReturn(new ApiProperty(identifier: true)); + $propertyMetadataFactory->create(Dummy::class, 'name', Argument::type('array'))->willReturn(new ApiProperty()); + + $definitionNameFactory = new DefinitionNameFactory(); + + $baseSchemaFactory = new BaseSchemaFactory( + resourceMetadataFactory: $resourceMetadataFactoryCollection->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactory->reveal(), + propertyMetadataFactory: $propertyMetadataFactory->reveal(), + definitionNameFactory: $definitionNameFactory, + ); + + $this->schemaFactory = new SchemaFactory( + $baseSchemaFactory, + [], + $definitionNameFactory, + $resourceMetadataFactoryCollection->reveal(), + ); + } + + public function testBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class); + + $this->assertTrue($resultSchema->isDefined()); + $this->assertSame('Dummy.jsonld', $resultSchema->getRootDefinitionKey()); + } + + public function testCustomFormatBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'json'); + + $this->assertTrue($resultSchema->isDefined()); + $this->assertSame('Dummy', $resultSchema->getRootDefinitionKey()); + } + + public function testHasRootDefinitionKeyBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class); + $definitions = $resultSchema->getDefinitions(); + $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); + + $this->assertTrue(isset($definitions[$rootDefinitionKey])); + $this->assertTrue(isset($definitions[$rootDefinitionKey]['allOf'][1]['properties'])); + $this->assertEquals($definitions[$rootDefinitionKey]['allOf'][0], ['$ref' => '#/definitions/HydraItemBaseSchema']); + + $properties = $definitions['HydraItemBaseSchema']['properties']; + $this->assertArrayHasKey('@context', $properties); + $this->assertEquals( + [ + 'oneOf' => [ + ['type' => 'string'], + [ + 'type' => 'object', + 'properties' => [ + '@vocab' => [ + 'type' => 'string', + ], + 'hydra' => [ + 'type' => 'string', + 'enum' => [ContextBuilder::HYDRA_NS], + ], + ], + 'required' => ['@vocab', 'hydra'], + 'additionalProperties' => true, + ], + ], + ], + $properties['@context'] + ); + $this->assertArrayHasKey('@type', $properties); + $this->assertArrayHasKey('@id', $properties); + } + + public function testSchemaTypeBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, new GetCollection()); + $this->assertNull($resultSchema->getRootDefinitionKey()); + $hydraCollectionSchema = $resultSchema['definitions']['HydraCollectionBaseSchema']; + $properties = $hydraCollectionSchema['properties']; + $this->assertTrue(isset($properties['hydra:member'])); + $this->assertArrayHasKey('hydra:totalItems', $properties); + $this->assertArrayHasKey('hydra:view', $properties); + $this->assertArrayHasKey('hydra:search', $properties); + $this->assertArrayNotHasKey('@context', $properties); + + $this->assertTrue(isset($properties['hydra:view'])); + $this->assertArrayHasKey('properties', $properties['hydra:view']); + $this->assertArrayHasKey('hydra:first', $properties['hydra:view']['properties']); + $this->assertArrayHasKey('hydra:last', $properties['hydra:view']['properties']); + $this->assertArrayHasKey('hydra:previous', $properties['hydra:view']['properties']); + $this->assertArrayHasKey('hydra:next', $properties['hydra:view']['properties']); + + $forcedCollection = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, null, null, true); + $this->assertEquals($resultSchema['allOf'][0]['$ref'], $forcedCollection['allOf'][0]['$ref']); + } + + public function testSchemaTypeBuildSchemaWithoutPrefix(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, new GetCollection(), null, [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false]); + $this->assertNull($resultSchema->getRootDefinitionKey()); + $hydraCollectionSchema = $resultSchema['definitions']['HydraCollectionBaseSchema']; + $properties = $hydraCollectionSchema['properties']; + $this->assertArrayHasKey('totalItems', $properties); + $this->assertArrayHasKey('view', $properties); + $this->assertArrayHasKey('search', $properties); + } +} diff --git a/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php b/src/Hydra/Tests/Serializer/CollectionFiltersNormalizerTest.php similarity index 97% rename from tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php rename to src/Hydra/Tests/Serializer/CollectionFiltersNormalizerTest.php index 9e3de3267c5..6943b8db13c 100644 --- a/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/CollectionFiltersNormalizerTest.php @@ -11,21 +11,21 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\Serializer; +namespace ApiPlatform\Hydra\Tests\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface; use ApiPlatform\Doctrine\Orm\Filter\FilterInterface; -use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer; use ApiPlatform\Hydra\Serializer\CollectionNormalizer; +use ApiPlatform\Hydra\Tests\Fixtures\Dummy; +use ApiPlatform\Hydra\Tests\Fixtures\Foo; +use ApiPlatform\Hydra\Tests\Fixtures\NotAResource; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; -use ApiPlatform\Tests\Fixtures\Foo; -use ApiPlatform\Tests\Fixtures\NotAResource; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Metadata\ResourceClassResolverInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -40,9 +40,6 @@ class CollectionFiltersNormalizerTest extends TestCase { use ProphecyTrait; - /** - * @group legacy - */ public function testSupportsNormalization(): void { $decoratedProphecy = $this->prophesize(NormalizerInterface::class); diff --git a/src/Hydra/Tests/Serializer/CollectionNormalizerTest.php b/src/Hydra/Tests/Serializer/CollectionNormalizerTest.php new file mode 100644 index 00000000000..9dfc068a5ea --- /dev/null +++ b/src/Hydra/Tests/Serializer/CollectionNormalizerTest.php @@ -0,0 +1,375 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Serializer; + +use ApiPlatform\Hydra\Serializer\CollectionNormalizer; +use ApiPlatform\Hydra\Tests\Fixtures\Foo; +use ApiPlatform\JsonLd\ContextBuilder; +use ApiPlatform\JsonLd\ContextBuilderInterface; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Serializer\AbstractItemNormalizer; +use ApiPlatform\State\Pagination\PaginatorInterface; +use ApiPlatform\State\Pagination\PartialPaginatorInterface; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * @author Amrouche Hamza + * @author Kévin Dunglas + */ +class CollectionNormalizerTest extends TestCase +{ + use ProphecyTrait; + + public function testSupportsNormalize(): void + { + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $iriConvert = $this->prophesize(IriConverterInterface::class); + $contextBuilder = $this->prophesize(ContextBuilderInterface::class); + $contextBuilder->getResourceContextUri('Foo')->willReturn('/contexts/Foo'); + $iriConvert->getIriFromResource('Foo', UrlGeneratorInterface::ABS_PATH, Argument::any(), Argument::any())->willReturn('/foos'); + + $normalizer = new CollectionNormalizer($contextBuilder->reveal(), $resourceClassResolverProphecy->reveal(), $iriConvert->reveal()); + + $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, ['resource_class' => 'Foo'])); + $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, ['resource_class' => 'Foo', 'api_sub_level' => true])); + $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, [])); + $this->assertTrue($normalizer->supportsNormalization(new \ArrayObject(), CollectionNormalizer::FORMAT, ['resource_class' => 'Foo'])); + $this->assertFalse($normalizer->supportsNormalization([], 'xml', ['resource_class' => 'Foo'])); + $this->assertFalse($normalizer->supportsNormalization(new \ArrayObject(), 'xml', ['resource_class' => 'Foo'])); + $this->assertEmpty($normalizer->getSupportedTypes('xml')); + $this->assertSame([ + 'native-array' => true, + '\Traversable' => true, + ], $normalizer->getSupportedTypes($normalizer::FORMAT)); + } + + public function testNormalizeResourceCollection(): void + { + $fooOne = new Foo(); + $fooOne->id = 1; + $fooOne->bar = 'baz'; + + $fooThree = new Foo(); + $fooThree->id = 3; + $fooThree->bar = 'bzz'; + + $data = [$fooOne, $fooThree]; + + $normalizedFooOne = [ + '@id' => '/foos/1', + '@type' => 'Foo', + 'bar' => 'baz', + ]; + + $normalizedFooThree = [ + '@id' => '/foos/3', + '@type' => 'Foo', + 'bar' => 'bzz', + ]; + + $contextBuilderProphecy = $this->prophesize(ContextBuilderInterface::class); + $contextBuilderProphecy->getResourceContextUri(Foo::class)->willReturn('/contexts/Foo'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($data, Foo::class)->willReturn(Foo::class); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(Foo::class, UrlGeneratorInterface::ABS_PATH, Argument::any(), Argument::any())->willReturn('/foos'); + + $delegateNormalizerProphecy = $this->prophesize(NormalizerInterface::class); + $delegateNormalizerProphecy->normalize($fooOne, CollectionNormalizer::FORMAT, Argument::allOf( + Argument::withEntry('resource_class', Foo::class), + Argument::withEntry('api_sub_level', true) + ))->willReturn($normalizedFooOne); + $delegateNormalizerProphecy->normalize($fooThree, CollectionNormalizer::FORMAT, Argument::allOf( + Argument::withEntry('resource_class', Foo::class), + Argument::withEntry('api_sub_level', true) + ))->willReturn($normalizedFooThree); + + $normalizer = new CollectionNormalizer($contextBuilderProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $iriConverterProphecy->reveal()); + $normalizer->setNormalizer($delegateNormalizerProphecy->reveal()); + + $actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ + 'operation_name' => 'get', + 'resource_class' => Foo::class, + ]); + + $this->assertEquals([ + '@context' => '/contexts/Foo', + '@id' => '/foos', + '@type' => 'hydra:Collection', + 'hydra:member' => [ + $normalizedFooOne, + $normalizedFooThree, + ], + 'hydra:totalItems' => 2, + ], $actual); + } + + public function testNormalizePaginator(): void + { + $this->assertEquals( + [ + '@context' => '/contexts/Foo', + '@id' => '/foo/1', + '@type' => 'hydra:Collection', + 'hydra:member' => [ + [ + 'name' => 'Kévin', + 'friend' => 'Smail', + ], + ], + 'hydra:totalItems' => 1312., + ], + $this->normalizePaginator() + ); + } + + public function testNormalizePartialPaginator(): void + { + $this->assertEquals( + [ + '@context' => '/contexts/Foo', + '@id' => '/foo/1', + '@type' => 'hydra:Collection', + 'hydra:member' => [ + 0 => [ + 'name' => 'Kévin', + 'friend' => 'Smail', + ], + ], + ], + $this->normalizePaginator(true) + ); + } + + private function normalizePaginator(bool $partial = false): array + { + $paginatorProphecy = $this->prophesize(PaginatorInterface::class); + if ($partial) { + $paginatorProphecy = $this->prophesize(PartialPaginatorInterface::class); + } + + if (!$partial) { + $paginatorProphecy->getTotalItems()->willReturn(1312); + } + + $paginatorProphecy->rewind()->will(function (): void {}); + $paginatorProphecy->valid()->willReturn(true, false); + $paginatorProphecy->current()->willReturn('foo'); + $paginatorProphecy->next()->will(function (): void {}); + + $serializer = $this->prophesize(SerializerInterface::class); + $serializer->willImplement(NormalizerInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($paginatorProphecy, 'Foo')->willReturn('Foo'); + + $iriConvert = $this->prophesize(IriConverterInterface::class); + $iriConvert->getIriFromResource('Foo', UrlGeneratorInterface::ABS_PATH, Argument::any(), Argument::any())->willReturn('/foo/1'); + + $contextBuilder = $this->prophesize(ContextBuilderInterface::class); + $contextBuilder->getResourceContextUri('Foo')->willReturn('/contexts/Foo'); + + $itemNormalizer = $this->prophesize(AbstractItemNormalizer::class); + $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ + 'jsonld_has_context' => true, + 'api_sub_level' => true, + 'resource_class' => 'Foo', + 'api_collection_sub_level' => true, + ])->willReturn(['name' => 'Kévin', 'friend' => 'Smail']); + + $normalizer = new CollectionNormalizer($contextBuilder->reveal(), $resourceClassResolverProphecy->reveal(), $iriConvert->reveal()); + $normalizer->setNormalizer($itemNormalizer->reveal()); + + return $normalizer->normalize($paginatorProphecy->reveal(), CollectionNormalizer::FORMAT, [ + 'resource_class' => 'Foo', + ]); + } + + public function testNormalizeIriOnlyResourceCollection(): void + { + $fooOne = new Foo(); + $fooOne->id = 1; + $fooOne->bar = 'baz'; + + $fooThree = new Foo(); + $fooThree->id = 3; + $fooThree->bar = 'bzz'; + + $data = [$fooOne, $fooThree]; + + $contextBuilderProphecy = $this->prophesize(ContextBuilderInterface::class); + $contextBuilderProphecy->getResourceContextUri(Foo::class)->willReturn('/contexts/Foo'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($data, Foo::class)->willReturn(Foo::class); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(Foo::class, UrlGeneratorInterface::ABS_PATH, Argument::any(), Argument::any())->willReturn('/foos'); + $iriConverterProphecy->getIriFromResource($fooOne)->willReturn('/foos/1'); + $iriConverterProphecy->getIriFromResource($fooThree)->willReturn('/foos/3'); + + $delegateNormalizerProphecy = $this->prophesize(NormalizerInterface::class); + + $normalizer = new CollectionNormalizer($contextBuilderProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $iriConverterProphecy->reveal()); + $normalizer->setNormalizer($delegateNormalizerProphecy->reveal()); + + $actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ + 'operation_name' => 'get', + 'iri_only' => true, + 'resource_class' => Foo::class, + ]); + + $this->assertEquals([ + '@context' => '/contexts/Foo', + '@id' => '/foos', + '@type' => 'hydra:Collection', + 'hydra:member' => [ + '/foos/1', + '/foos/3', + ], + 'hydra:totalItems' => 2, + ], $actual); + } + + public function testNormalizeIriOnlyEmbedContextResourceCollection(): void + { + $fooOne = new Foo(); + $fooOne->id = 1; + $fooOne->bar = 'baz'; + + $fooThree = new Foo(); + $fooThree->id = 3; + $fooThree->bar = 'bzz'; + + $data = [$fooOne, $fooThree]; + + $contextBuilderProphecy = $this->prophesize(ContextBuilderInterface::class); + $contextBuilderProphecy->getResourceContext(Foo::class)->willReturn([ + '@vocab' => 'http://localhost:8080/docs.jsonld#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'hydra:member' => [ + '@type' => '@id', + ], + ]); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($data, Foo::class)->willReturn(Foo::class); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(Foo::class, UrlGeneratorInterface::ABS_PATH, Argument::any(), Argument::any())->willReturn('/foos'); + $iriConverterProphecy->getIriFromResource($fooOne)->willReturn('/foos/1'); + $iriConverterProphecy->getIriFromResource($fooThree)->willReturn('/foos/3'); + + $delegateNormalizerProphecy = $this->prophesize(NormalizerInterface::class); + + $normalizer = new CollectionNormalizer($contextBuilderProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $iriConverterProphecy->reveal()); + $normalizer->setNormalizer($delegateNormalizerProphecy->reveal()); + + $actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ + 'operation_name' => 'get', + 'iri_only' => true, + 'jsonld_embed_context' => true, + 'resource_class' => Foo::class, + ]); + + $this->assertEquals([ + '@context' => [ + '@vocab' => 'http://localhost:8080/docs.jsonld#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'hydra:member' => [ + '@type' => '@id', + ], + ], + '@id' => '/foos', + '@type' => 'hydra:Collection', + 'hydra:member' => [ + '/foos/1', + '/foos/3', + ], + 'hydra:totalItems' => 2, + ], $actual); + } + + public function testNormalizeResourceCollectionWithoutPrefix(): void + { + $fooOne = new Foo(); + $fooOne->id = 1; + $fooOne->bar = 'baz'; + + $fooThree = new Foo(); + $fooThree->id = 3; + $fooThree->bar = 'bzz'; + + $data = [$fooOne, $fooThree]; + + $normalizedFooOne = [ + '@id' => '/foos/1', + '@type' => 'Foo', + 'bar' => 'baz', + ]; + + $normalizedFooThree = [ + '@id' => '/foos/3', + '@type' => 'Foo', + 'bar' => 'bzz', + ]; + + $contextBuilderProphecy = $this->prophesize(ContextBuilderInterface::class); + $contextBuilderProphecy->getResourceContextUri(Foo::class)->willReturn('/contexts/Foo'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($data, Foo::class)->willReturn(Foo::class); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(Foo::class, UrlGeneratorInterface::ABS_PATH, Argument::any(), Argument::any())->willReturn('/foos'); + + $delegateNormalizerProphecy = $this->prophesize(NormalizerInterface::class); + $delegateNormalizerProphecy->normalize($fooOne, CollectionNormalizer::FORMAT, Argument::allOf( + Argument::withEntry('resource_class', Foo::class), + Argument::withEntry('api_sub_level', true) + ))->willReturn($normalizedFooOne); + $delegateNormalizerProphecy->normalize($fooThree, CollectionNormalizer::FORMAT, Argument::allOf( + Argument::withEntry('resource_class', Foo::class), + Argument::withEntry('api_sub_level', true) + ))->willReturn($normalizedFooThree); + + $normalizer = new CollectionNormalizer($contextBuilderProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $iriConverterProphecy->reveal()); + $normalizer->setNormalizer($delegateNormalizerProphecy->reveal()); + + $actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ + 'operation_name' => 'get', + 'resource_class' => Foo::class, + ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false, + ]); + + $this->assertEquals([ + '@context' => '/contexts/Foo', + '@id' => '/foos', + '@type' => 'Collection', + 'member' => [ + $normalizedFooOne, + $normalizedFooThree, + ], + 'totalItems' => 2, + ], $actual); + } +} diff --git a/src/Hydra/Tests/Serializer/ConstraintViolationNormalizerTest.php b/src/Hydra/Tests/Serializer/ConstraintViolationNormalizerTest.php new file mode 100644 index 00000000000..96f1532f898 --- /dev/null +++ b/src/Hydra/Tests/Serializer/ConstraintViolationNormalizerTest.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Serializer; + +use ApiPlatform\Hydra\Serializer\ConstraintViolationListNormalizer; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Validator\Constraints\NotNull; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\ConstraintViolationListInterface; + +/** + * @author Kévin Dunglas + */ +class ConstraintViolationNormalizerTest extends TestCase +{ + use ProphecyTrait; + + public function testSupportNormalization(): void + { + $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); + + $normalizer = new ConstraintViolationListNormalizer([], $nameConverterProphecy->reveal()); + + $this->assertTrue($normalizer->supportsNormalization(new ConstraintViolationList(), ConstraintViolationListNormalizer::FORMAT, ['api_error_resource' => true])); + $this->assertFalse($normalizer->supportsNormalization(new ConstraintViolationList(), 'xml', ['api_error_resource' => true])); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ConstraintViolationListNormalizer::FORMAT, ['api_error_resource' => true])); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ConstraintViolationListNormalizer::FORMAT)); + $this->assertEmpty($normalizer->getSupportedTypes('json')); + $this->assertSame([ConstraintViolationListInterface::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); + } + + #[\PHPUnit\Framework\Attributes\DataProvider('nameConverterAndPayloadFieldsProvider')] + public function testNormalize(callable $nameConverterFactory, ?array $fields, array $expected): void + { + $normalizer = new ConstraintViolationListNormalizer($fields, $nameConverterFactory($this)); + + // Note : we use NotNull constraint and not Constraint class because Constraint is abstract + $constraint = new NotNull(); + $constraint->payload = ['severity' => 'warning', 'anotherField2' => 'aValue']; + $list = new ConstraintViolationList([ + new ConstraintViolation('a', 'b', [], 'c', 'd', 'e', null, 'f24bdbad0becef97a6887238aa58221c', $constraint), + new ConstraintViolation('1', '2', [], '3', '4', '5'), + ]); + + $this->assertSame($expected, $normalizer->normalize($list)); + } + + public static function nameConverterAndPayloadFieldsProvider(): iterable + { + $basicExpectation = [ + [ + 'propertyPath' => 'd', + 'message' => 'a', + 'code' => 'f24bdbad0becef97a6887238aa58221c', + ], + [ + 'propertyPath' => '4', + 'message' => '1', + 'code' => null, + ], + ]; + + $nameConverterBasedExpectation = [ + [ + 'propertyPath' => '_d', + 'message' => 'a', + 'code' => 'f24bdbad0becef97a6887238aa58221c', + ], + [ + 'propertyPath' => '_4', + 'message' => '1', + 'code' => null, + ], + ]; + + $advancedNameConverterFactory = function (self $that) { + $advancedNameConverterProphecy = $that->prophesize(AdvancedNameConverterInterface::class); + $advancedNameConverterProphecy->normalize(Argument::type('string'), null, Argument::type('string'))->will(fn ($args): string => '_'.$args[0]); + + return $advancedNameConverterProphecy->reveal(); + }; + + $nameConverterFactory = function (self $that) { + $nameConverterProphecy = $that->prophesize(NameConverterInterface::class); + $nameConverterProphecy->normalize(Argument::type('string'))->will(fn ($args): string => '_'.$args[0]); + + return $nameConverterProphecy->reveal(); + }; + + $nullNameConverterFactory = fn () => null; + + $expected = $nameConverterBasedExpectation; + $expected[0]['payload'] = ['severity' => 'warning']; + yield [$advancedNameConverterFactory, ['severity', 'anotherField1'], $expected]; + yield [$nameConverterFactory, ['severity', 'anotherField1'], $expected]; + $expected = $basicExpectation; + $expected[0]['payload'] = ['severity' => 'warning']; + yield [$nullNameConverterFactory, ['severity', 'anotherField1'], $expected]; + + $expected = $nameConverterBasedExpectation; + $expected[0]['payload'] = ['severity' => 'warning', 'anotherField2' => 'aValue']; + yield [$advancedNameConverterFactory, null, $expected]; + yield [$nameConverterFactory, null, $expected]; + $expected = $basicExpectation; + $expected[0]['payload'] = ['severity' => 'warning', 'anotherField2' => 'aValue']; + yield [$nullNameConverterFactory, null, $expected]; + + yield [$advancedNameConverterFactory, [], $nameConverterBasedExpectation]; + yield [$nameConverterFactory, [], $nameConverterBasedExpectation]; + yield [$nullNameConverterFactory, [], $basicExpectation]; + } +} diff --git a/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php b/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php new file mode 100644 index 00000000000..d04c92cb3b7 --- /dev/null +++ b/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php @@ -0,0 +1,931 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Serializer; + +use ApiPlatform\Documentation\Documentation; +use ApiPlatform\Hydra\Serializer\DocumentationNormalizer; +use ApiPlatform\Hydra\Tests\Fixtures\CustomConverter; +use ApiPlatform\JsonLd\ContextBuilder; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\Put; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\TypeInfo\Type; + +use const ApiPlatform\JsonLd\HYDRA_CONTEXT; + +/** + * @author Amrouche Hamza + */ +class DocumentationNormalizerTest extends TestCase +{ + use ProphecyTrait; + + #[IgnoreDeprecations] + public function testNormalize(): void + { + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('dummy')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('dummy', [ + (new ApiResource())->withShortName('dummy')->withDescription('dummy')->withTypes(['#dummy'])->withOperations(new Operations([ + 'get' => (new Get())->withHydraContext(['hydra:foo' => 'bar', 'hydra:title' => 'foobar'])->withTypes(['#dummy'])->withShortName('dummy'), + 'put' => (new Put())->withShortName('dummy'), + 'get_collection' => (new GetCollection())->withShortName('dummy'), + 'post' => (new Post())->withShortName('dummy'), + ])), + (new ApiResource())->withShortName('relatedDummy')->withOperations(new Operations(['get' => (new Get())->withTypes(['#relatedDummy'])->withShortName('relatedDummy')])), + ])); + $resourceMetadataFactoryProphecy->create('relatedDummy')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('relatedDummy', [ + (new ApiResource())->withShortName('relatedDummy')->withOperations(new Operations(['get' => (new Get())->withShortName('relatedDummy')])), + ])); + + $this->doTestNormalize($resourceMetadataFactoryProphecy->reveal()); + } + + private function doTestNormalize($resourceMetadataFactory = null): void + { + $title = 'Test Api'; + $desc = 'test ApiGerard'; + $version = '0.0.0'; + $documentation = new Documentation(new ResourceNameCollection(['dummy' => 'dummy']), $title, $desc, $version); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create('dummy', [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['name', 'description', 'nameConverted', 'relatedDummy', 'iri'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create('dummy', 'name', Argument::type('array'))->shouldBeCalled()->willReturn( + (new ApiProperty())->withNativeType(Type::string())->withDescription('name')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true) + ); + $propertyMetadataFactoryProphecy->create('dummy', 'description', Argument::type('array'))->shouldBeCalled()->willReturn( + (new ApiProperty())->withNativeType(Type::string())->withDescription('description')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withJsonldContext(['@type' => '@id']) + ); + $propertyMetadataFactoryProphecy->create('dummy', 'nameConverted', Argument::type('array'))->shouldBeCalled()->willReturn( + (new ApiProperty())->withNativeType(Type::string())->withDescription('name converted')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true) + ); + $propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::collection(Type::object('dummy'), Type::object('relatedDummy')))->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)); // @phpstan-ignore-line + $propertyMetadataFactoryProphecy->create('dummy', 'iri', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withIris(['https://schema.org/Dummy'])); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true); + + $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('api_entrypoint')->willReturn('/')->shouldBeCalledTimes(1); + $urlGenerator->generate('api_doc', ['_format' => 'jsonld'])->willReturn('/doc')->shouldBeCalledTimes(1); + + $urlGenerator->generate('api_doc', ['_format' => 'jsonld'], 0)->willReturn('/doc')->shouldBeCalledTimes(1); + + $documentationNormalizer = new DocumentationNormalizer( + $resourceMetadataFactory, + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $urlGenerator->reveal(), + new CustomConverter() + ); + + $expected = [ + '@context' => [ + HYDRA_CONTEXT, + [ + '@vocab' => '/doc#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xmls' => 'http://www.w3.org/2001/XMLSchema#', + 'owl' => 'http://www.w3.org/2002/07/owl#', + 'schema' => 'https://schema.org/', + 'domain' => [ + '@id' => 'rdfs:domain', + '@type' => '@id', + ], + 'range' => [ + '@id' => 'rdfs:range', + '@type' => '@id', + ], + 'subClassOf' => [ + '@id' => 'rdfs:subClassOf', + '@type' => '@id', + ], + ], + ], + '@id' => '/doc', + '@type' => 'hydra:ApiDocumentation', + 'hydra:title' => 'Test Api', + 'hydra:description' => 'test ApiGerard', + 'hydra:supportedClass' => [ + [ + '@id' => '#dummy', + '@type' => 'hydra:Class', + 'hydra:title' => 'dummy', + 'hydra:description' => 'dummy', + 'hydra:supportedProperty' => [ + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/name', + '@type' => 'rdf:Property', + 'label' => 'name', + 'domain' => '#dummy', + 'range' => 'xmls:string', + ], + 'hydra:title' => 'name', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'name', + ], + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/description', + '@type' => 'rdf:Property', + 'label' => 'description', + 'domain' => '#dummy', + 'range' => '@id', + ], + 'hydra:title' => 'description', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'description', + ], + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/name_converted', + '@type' => 'rdf:Property', + 'label' => 'name_converted', + 'domain' => '#dummy', + 'range' => 'xmls:string', + ], + 'hydra:title' => 'name_converted', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'name converted', + ], + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/relatedDummy', + '@type' => 'rdf:Property', + 'label' => 'relatedDummy', + 'domain' => '#dummy', + 'range' => '#relatedDummy', + ], + 'hydra:title' => 'relatedDummy', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'This is a name.', + ], + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => 'https://schema.org/Dummy', + '@type' => 'rdf:Property', + 'label' => 'iri', + 'domain' => '#dummy', + ], + 'hydra:title' => 'iri', + 'hydra:required' => null, + 'hydra:readable' => null, + 'hydra:writeable' => false, + ], + ], + 'hydra:supportedOperation' => [ + [ + '@type' => ['hydra:Operation', 'schema:FindAction'], + 'hydra:method' => 'GET', + 'hydra:title' => 'foobar', + 'returns' => 'dummy', + 'hydra:foo' => 'bar', + 'hydra:description' => 'Retrieves a dummy resource.', + ], + [ + '@type' => ['hydra:Operation', 'schema:ReplaceAction'], + 'expects' => 'dummy', + 'hydra:method' => 'PUT', + 'hydra:title' => 'putdummy', + 'hydra:description' => 'Replaces the dummy resource.', + 'returns' => 'dummy', + ], + [ + '@type' => ['hydra:Operation', 'schema:FindAction'], + 'hydra:method' => 'GET', + 'hydra:title' => 'getrelatedDummy', + 'hydra:description' => 'Retrieves a relatedDummy resource.', + 'returns' => 'relatedDummy', + ], + ], + ], + [ + '@id' => '#Entrypoint', + '@type' => 'hydra:Class', + 'hydra:title' => 'Entrypoint', + 'hydra:supportedProperty' => [ + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#Entrypoint/dummy', + '@type' => 'hydra:Link', + 'domain' => '#Entrypoint', + 'range' => [ + ['@id' => 'hydra:Collection'], + [ + 'owl:equivalentClass' => [ + 'owl:onProperty' => ['@id' => 'hydra:member'], + 'owl:allValuesFrom' => ['@id' => '#dummy'], + ], + ], + ], + 'owl:maxCardinality' => 1, + 'hydra:supportedOperation' => [ + [ + '@type' => ['hydra:Operation', 'schema:FindAction'], + 'hydra:method' => 'GET', + 'hydra:title' => 'getdummyCollection', + 'hydra:description' => 'Retrieves the collection of dummy resources.', + 'returns' => 'hydra:Collection', + ], + [ + '@type' => ['hydra:Operation', 'schema:CreateAction'], + 'expects' => 'dummy', + 'hydra:method' => 'POST', + 'hydra:title' => 'postdummy', + 'hydra:description' => 'Creates a dummy resource.', + 'returns' => 'dummy', + ], + ], + ], + 'hydra:title' => 'getdummyCollection', + 'hydra:description' => 'The collection of dummy resources', + 'hydra:readable' => true, + 'hydra:writeable' => false, + ], + ], + 'hydra:supportedOperation' => [ + '@type' => 'hydra:Operation', + 'hydra:method' => 'GET', + 'hydra:title' => 'index', + 'hydra:description' => 'The API Entrypoint.', + 'hydra:returns' => 'Entrypoint', + ], + ], + [ + '@id' => '#ConstraintViolationList', + '@type' => 'hydra:Class', + 'hydra:title' => 'ConstraintViolationList', + 'hydra:description' => 'A constraint violation List.', + 'hydra:supportedProperty' => [ + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#ConstraintViolationList/propertyPath', + '@type' => 'rdf:Property', + 'rdfs:label' => 'propertyPath', + 'domain' => '#ConstraintViolationList', + 'range' => 'xmls:string', + ], + 'hydra:title' => 'propertyPath', + 'hydra:description' => 'The property path of the violation', + 'hydra:readable' => true, + 'hydra:writeable' => false, + ], + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#ConstraintViolationList/message', + '@type' => 'rdf:Property', + 'rdfs:label' => 'message', + 'domain' => '#ConstraintViolationList', + 'range' => 'xmls:string', + ], + 'hydra:title' => 'message', + 'hydra:description' => 'The message associated with the violation', + 'hydra:readable' => true, + 'hydra:writeable' => false, + ], + ], + ], + ], + 'hydra:entrypoint' => '/', + ]; + + $this->assertEquals($expected, $documentationNormalizer->normalize($documentation)); + $this->assertTrue($documentationNormalizer->supportsNormalization($documentation, 'jsonld')); + $this->assertFalse($documentationNormalizer->supportsNormalization($documentation, 'hal')); + $this->assertEmpty($documentationNormalizer->getSupportedTypes('json')); + $this->assertSame([Documentation::class => true], $documentationNormalizer->getSupportedTypes($documentationNormalizer::FORMAT)); + } + + public function testNormalizeInputOutputClass(): void + { + $title = 'Test Api'; + $desc = 'test ApiGerard'; + $version = '0.0.0'; + $documentation = new Documentation(new ResourceNameCollection(['dummy' => 'dummy']), $title, $desc, $version); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create('inputClass', Argument::type('array'))->shouldBeCalled()->willReturn(new PropertyNameCollection(['a', 'b'])); + $propertyNameCollectionFactoryProphecy->create('outputClass', Argument::type('array'))->shouldBeCalled()->willReturn(new PropertyNameCollection(['c', 'd'])); + $propertyNameCollectionFactoryProphecy->create('dummy', Argument::type('array'))->shouldBeCalled()->willReturn(new PropertyNameCollection([])); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('dummy')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('dummy', [ + (new ApiResource())->withShortName('dummy')->withDescription('dummy')->withTypes(['#dummy'])->withOperations(new Operations([ + 'get' => (new Get())->withTypes(['#dummy'])->withShortName('dummy')->withInput(['class' => 'inputClass'])->withOutput(['class' => 'outputClass']), + 'put' => (new Put())->withShortName('dummy')->withInput(['class' => null])->withOutput(['class' => 'outputClass']), + 'get_collection' => (new GetCollection())->withShortName('dummy')->withInput(['class' => 'inputClass'])->withOutput(['class' => 'outputClass']), + 'post' => (new Post())->withShortName('dummy')->withOutput(['class' => null])->withInput(['class' => 'inputClass']), + ])), + ])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create('inputClass', 'a', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('a')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)); + $propertyMetadataFactoryProphecy->create('inputClass', 'b', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('b')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)); + $propertyMetadataFactoryProphecy->create('outputClass', 'c', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('c')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)); + $propertyMetadataFactoryProphecy->create('outputClass', 'd', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('d')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true); + + $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('api_entrypoint')->willReturn('/')->shouldBeCalledTimes(1); + $urlGenerator->generate('api_doc', ['_format' => 'jsonld'])->willReturn('/doc')->shouldBeCalledTimes(1); + $urlGenerator->generate('api_doc', ['_format' => 'jsonld'], 0)->willReturn('/doc')->shouldBeCalledTimes(1); + + $documentationNormalizer = new DocumentationNormalizer( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $urlGenerator->reveal() + ); + + $expected = [ + '@id' => '#dummy', + '@type' => 'hydra:Class', + 'hydra:title' => 'dummy', + 'hydra:supportedProperty' => [ + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/a', + '@type' => 'rdf:Property', + 'label' => 'a', + 'domain' => '#dummy', + 'range' => 'xmls:string', + ], + 'hydra:title' => 'a', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'a', + ], + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/b', + '@type' => 'rdf:Property', + 'label' => 'b', + 'domain' => '#dummy', + 'range' => 'xmls:string', + ], + 'hydra:title' => 'b', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'b', + ], + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/c', + '@type' => 'rdf:Property', + 'label' => 'c', + 'domain' => '#dummy', + 'range' => 'xmls:string', + ], + 'hydra:title' => 'c', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'c', + ], + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/d', + '@type' => 'rdf:Property', + 'label' => 'd', + 'domain' => '#dummy', + 'range' => 'xmls:string', + ], + 'hydra:title' => 'd', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'd', + ], + ], + 'hydra:supportedOperation' => [ + [ + '@type' => [ + 'hydra:Operation', + 'schema:FindAction', + ], + 'hydra:method' => 'GET', + 'hydra:title' => 'getdummy', + 'hydra:description' => 'Retrieves a dummy resource.', + 'returns' => 'dummy', + ], + [ + '@type' => [ + 'hydra:Operation', + 'schema:ReplaceAction', + ], + 'expects' => 'owl:Nothing', + 'hydra:method' => 'PUT', + 'hydra:title' => 'putdummy', + 'hydra:description' => 'Replaces the dummy resource.', + 'returns' => 'dummy', + ], + ], + 'hydra:description' => 'dummy', + ]; + + $doc = $documentationNormalizer->normalize($documentation); + $this->assertEquals($expected, $doc['hydra:supportedClass'][0]); + } + + public function testHasHydraContext(): void + { + $title = 'Test Api'; + $desc = 'test ApiGerard'; + $version = '0.0.0'; + $documentation = new Documentation(new ResourceNameCollection(['dummy' => 'dummy']), $title, $desc, $version); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create('dummy', Argument::type('array'))->shouldBeCalled()->willReturn(new PropertyNameCollection(['name'])); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('dummy')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('dummy', [ + (new ApiResource())->withShortName('dummy')->withDescription('dummy')->withTypes(['#dummy'])->withOperations(new Operations([ + 'get' => (new Get())->withTypes(['#dummy'])->withShortName('dummy')->withInput(['class' => 'inputClass'])->withOutput(['class' => 'outputClass']), + ])), + ])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create('dummy', 'name', Argument::type('array'))->shouldBeCalled()->willReturn( + (new ApiProperty()) + ->withNativeType(Type::string()) + ->withDescription('b') + ->withReadable(true) + ->withWritable(true) + ->withJsonldContext([ + 'hydra:property' => [ + '@type' => 'https://schema.org/Enumeration', + ], + ]) + ); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true); + + $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('api_entrypoint')->willReturn('/')->shouldBeCalledTimes(1); + $urlGenerator->generate('api_doc', ['_format' => 'jsonld'])->willReturn('/doc')->shouldBeCalledTimes(1); + $urlGenerator->generate('api_doc', ['_format' => 'jsonld'], 0)->willReturn('/doc')->shouldBeCalledTimes(1); + + $documentationNormalizer = new DocumentationNormalizer( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $urlGenerator->reveal() + ); + + $this->assertEquals([ + '@id' => '#dummy/name', + '@type' => 'https://schema.org/Enumeration', + 'label' => 'name', + 'domain' => '#dummy', + 'range' => 'xmls:string', + ], $documentationNormalizer->normalize($documentation)['hydra:supportedClass'][0]['hydra:supportedProperty'][0]['hydra:property']); + } + + public function testNormalizeWithoutPrefix(): void + { + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('dummy')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('dummy', [ + (new ApiResource())->withShortName('dummy')->withDescription('dummy')->withTypes(['#dummy'])->withOperations(new Operations([ + 'get' => (new Get())->withHydraContext(['foo' => 'bar', 'title' => 'foobar'])->withTypes(['#dummy'])->withShortName('dummy'), + 'put' => (new Put())->withShortName('dummy'), + 'get_collection' => (new GetCollection())->withShortName('dummy'), + 'post' => (new Post())->withShortName('dummy'), + ])), + (new ApiResource())->withShortName('relatedDummy')->withOperations(new Operations(['get' => (new Get())->withTypes(['#relatedDummy'])->withShortName('relatedDummy')])), + ])); + $resourceMetadataFactoryProphecy->create('relatedDummy')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('relatedDummy', [ + (new ApiResource())->withShortName('relatedDummy')->withOperations(new Operations(['get' => (new Get())->withShortName('relatedDummy')])), + ])); + + $title = 'Test Api'; + $desc = 'test ApiGerard'; + $version = '0.0.0'; + $documentation = new Documentation(new ResourceNameCollection(['dummy' => 'dummy']), $title, $desc, $version); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create('dummy', [])->shouldBeCalled()->willReturn(new PropertyNameCollection(['name', 'description', 'nameConverted', 'relatedDummy', 'iri'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create('dummy', 'name', Argument::type('array'))->shouldBeCalled()->willReturn( + (new ApiProperty())->withNativeType(Type::string())->withDescription('name')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true) + ); + $propertyMetadataFactoryProphecy->create('dummy', 'description', Argument::type('array'))->shouldBeCalled()->willReturn( + (new ApiProperty())->withNativeType(Type::string())->withDescription('description')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)->withJsonldContext(['@type' => '@id']) + ); + $propertyMetadataFactoryProphecy->create('dummy', 'nameConverted', Argument::type('array'))->shouldBeCalled()->willReturn( + (new ApiProperty())->withNativeType(Type::string())->withDescription('name converted')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true) + ); + $propertyMetadataFactoryProphecy->create('dummy', 'relatedDummy', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withNativeType(Type::collection(Type::object('dummy'), Type::object('relatedDummy')))->withDescription('This is a name.')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true)); // @phpstan-ignore-line + $propertyMetadataFactoryProphecy->create('dummy', 'iri', Argument::type('array'))->shouldBeCalled()->willReturn((new ApiProperty())->withIris(['https://schema.org/Dummy'])); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true); + + $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('api_entrypoint')->willReturn('/')->shouldBeCalledTimes(1); + $urlGenerator->generate('api_doc', ['_format' => 'jsonld'])->willReturn('/doc')->shouldBeCalledTimes(1); + + $urlGenerator->generate('api_doc', ['_format' => 'jsonld'], 0)->willReturn('/doc')->shouldBeCalledTimes(1); + + $documentationNormalizer = new DocumentationNormalizer( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $urlGenerator->reveal(), + new CustomConverter() + ); + + $expected = [ + '@context' => [ + HYDRA_CONTEXT, + [ + '@vocab' => '/doc#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xmls' => 'http://www.w3.org/2001/XMLSchema#', + 'owl' => 'http://www.w3.org/2002/07/owl#', + 'schema' => 'https://schema.org/', + 'domain' => [ + '@id' => 'rdfs:domain', + '@type' => '@id', + ], + 'range' => [ + '@id' => 'rdfs:range', + '@type' => '@id', + ], + 'subClassOf' => [ + '@id' => 'rdfs:subClassOf', + '@type' => '@id', + ], + ], + ], + '@id' => '/doc', + '@type' => 'ApiDocumentation', + 'title' => 'Test Api', + 'description' => 'test ApiGerard', + 'supportedClass' => [ + [ + '@id' => '#dummy', + '@type' => 'Class', + 'title' => 'dummy', + 'description' => 'dummy', + 'supportedProperty' => [ + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#dummy/name', + '@type' => 'rdf:Property', + 'label' => 'name', + 'domain' => '#dummy', + 'range' => 'xmls:string', + ], + 'title' => 'name', + 'required' => false, + 'readable' => true, + 'writeable' => true, + 'description' => 'name', + ], + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#dummy/description', + '@type' => 'rdf:Property', + 'label' => 'description', + 'domain' => '#dummy', + 'range' => '@id', + ], + 'title' => 'description', + 'required' => false, + 'readable' => true, + 'writeable' => true, + 'description' => 'description', + ], + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#dummy/name_converted', + '@type' => 'rdf:Property', + 'label' => 'name_converted', + 'domain' => '#dummy', + 'range' => 'xmls:string', + ], + 'title' => 'name_converted', + 'required' => false, + 'readable' => true, + 'writeable' => true, + 'description' => 'name converted', + ], + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#dummy/relatedDummy', + '@type' => 'rdf:Property', + 'label' => 'relatedDummy', + 'domain' => '#dummy', + 'range' => '#relatedDummy', + ], + 'title' => 'relatedDummy', + 'required' => false, + 'readable' => true, + 'writeable' => true, + 'description' => 'This is a name.', + ], + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => 'https://schema.org/Dummy', + '@type' => 'rdf:Property', + 'label' => 'iri', + 'domain' => '#dummy', + ], + 'title' => 'iri', + 'required' => null, + 'readable' => null, + 'writeable' => false, + ], + ], + 'supportedOperation' => [ + [ + '@type' => ['Operation', 'schema:FindAction'], + 'method' => 'GET', + 'title' => 'foobar', + 'returns' => 'dummy', + 'foo' => 'bar', + 'description' => 'Retrieves a dummy resource.', + ], + [ + '@type' => ['Operation', 'schema:ReplaceAction'], + 'expects' => 'dummy', + 'method' => 'PUT', + 'title' => 'putdummy', + 'description' => 'Replaces the dummy resource.', + 'returns' => 'dummy', + ], + [ + '@type' => ['Operation', 'schema:FindAction'], + 'method' => 'GET', + 'title' => 'getrelatedDummy', + 'description' => 'Retrieves a relatedDummy resource.', + 'returns' => 'relatedDummy', + ], + ], + ], + [ + '@id' => '#Entrypoint', + '@type' => 'Class', + 'title' => 'Entrypoint', + 'supportedProperty' => [ + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#Entrypoint/dummy', + '@type' => 'Link', + 'domain' => '#Entrypoint', + 'range' => [ + ['@id' => 'Collection'], + [ + 'owl:equivalentClass' => [ + 'owl:onProperty' => ['@id' => 'member'], + 'owl:allValuesFrom' => ['@id' => '#dummy'], + ], + ], + ], + 'owl:maxCardinality' => 1, + 'supportedOperation' => [ + [ + '@type' => ['Operation', 'schema:FindAction'], + 'method' => 'GET', + 'title' => 'getdummyCollection', + 'description' => 'Retrieves the collection of dummy resources.', + 'returns' => 'Collection', + ], + [ + '@type' => ['Operation', 'schema:CreateAction'], + 'expects' => 'dummy', + 'method' => 'POST', + 'title' => 'postdummy', + 'description' => 'Creates a dummy resource.', + 'returns' => 'dummy', + ], + ], + ], + 'title' => 'getdummyCollection', + 'description' => 'The collection of dummy resources', + 'readable' => true, + 'writeable' => false, + ], + ], + 'supportedOperation' => [ + '@type' => 'Operation', + 'method' => 'GET', + 'title' => 'index', + 'description' => 'The API Entrypoint.', + 'returns' => 'Entrypoint', + ], + ], + [ + '@id' => '#ConstraintViolationList', + '@type' => 'Class', + 'title' => 'ConstraintViolationList', + 'description' => 'A constraint violation List.', + 'supportedProperty' => [ + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#ConstraintViolationList/propertyPath', + '@type' => 'rdf:Property', + 'rdfs:label' => 'propertyPath', + 'domain' => '#ConstraintViolationList', + 'range' => 'xmls:string', + ], + 'title' => 'propertyPath', + 'description' => 'The property path of the violation', + 'readable' => true, + 'writeable' => false, + ], + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#ConstraintViolationList/message', + '@type' => 'rdf:Property', + 'rdfs:label' => 'message', + 'domain' => '#ConstraintViolationList', + 'range' => 'xmls:string', + ], + 'title' => 'message', + 'description' => 'The message associated with the violation', + 'readable' => true, + 'writeable' => false, + ], + ], + ], + ], + 'entrypoint' => '/', + ]; + + $this->assertEquals($expected, $documentationNormalizer->normalize($documentation, null, [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false])); + } + + public function testNormalizeNoEntrypointAndHideHydraOperation(): void + { + $title = 'Test Api'; + $desc = 'test ApiGerard'; + $version = '0.0.0'; + $documentation = new Documentation(new ResourceNameCollection(['dummy' => 'dummy']), $title, $desc, $version); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('dummy')->willReturn(new ResourceMetadataCollection('dummy', [ + (new ApiResource())->withHideHydraOperation(true)->withOperations(new Operations([ + 'get' => new Get(), + ])), + ])); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('api_doc', ['_format' => 'jsonld'], Argument::any())->willReturn('/doc'); + + $documentationNormalizer = new DocumentationNormalizer( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $urlGenerator->reveal(), + new CustomConverter(), + [], + false, + ); + + $expected = [ + '@context' => [ + HYDRA_CONTEXT, + [ + '@vocab' => '/doc#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xmls' => 'http://www.w3.org/2001/XMLSchema#', + 'owl' => 'http://www.w3.org/2002/07/owl#', + 'schema' => 'https://schema.org/', + 'domain' => [ + '@id' => 'rdfs:domain', + '@type' => '@id', + ], + 'range' => [ + '@id' => 'rdfs:range', + '@type' => '@id', + ], + 'subClassOf' => [ + '@id' => 'rdfs:subClassOf', + '@type' => '@id', + ], + ], + ], + '@id' => '/doc', + '@type' => 'ApiDocumentation', + 'title' => 'Test Api', + 'description' => 'test ApiGerard', + 'supportedClass' => [ + [ + '@id' => '#ConstraintViolationList', + '@type' => 'Class', + 'title' => 'ConstraintViolationList', + 'description' => 'A constraint violation List.', + 'supportedProperty' => [ + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#ConstraintViolationList/propertyPath', + '@type' => 'rdf:Property', + 'rdfs:label' => 'propertyPath', + 'domain' => '#ConstraintViolationList', + 'range' => 'xmls:string', + ], + 'title' => 'propertyPath', + 'description' => 'The property path of the violation', + 'readable' => true, + 'writeable' => false, + ], + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#ConstraintViolationList/message', + '@type' => 'rdf:Property', + 'rdfs:label' => 'message', + 'domain' => '#ConstraintViolationList', + 'range' => 'xmls:string', + ], + 'title' => 'message', + 'description' => 'The message associated with the violation', + 'readable' => true, + 'writeable' => false, + ], + ], + ], + ], + ]; + + $this->assertEquals($expected, $documentationNormalizer->normalize($documentation, null, [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false])); + } +} diff --git a/src/Hydra/Tests/Serializer/EntrypointNormalizerTest.php b/src/Hydra/Tests/Serializer/EntrypointNormalizerTest.php new file mode 100644 index 00000000000..ab115fa026b --- /dev/null +++ b/src/Hydra/Tests/Serializer/EntrypointNormalizerTest.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Hydra\Tests\Serializer; + +use ApiPlatform\Documentation\Entrypoint; +use ApiPlatform\Hydra\Serializer\EntrypointNormalizer; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\FooDummy; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; + +/** + * @author Kévin Dunglas + */ +class EntrypointNormalizerTest extends TestCase +{ + use ProphecyTrait; + + public function testSupportNormalization(): void + { + $collection = new ResourceNameCollection(); + $entrypoint = new Entrypoint($collection); + + $factoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + + $normalizer = new EntrypointNormalizer($factoryProphecy->reveal(), $iriConverterProphecy->reveal(), $urlGeneratorProphecy->reveal()); + + $this->assertTrue($normalizer->supportsNormalization($entrypoint, EntrypointNormalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization($entrypoint, 'json')); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), EntrypointNormalizer::FORMAT)); + $this->assertEmpty($normalizer->getSupportedTypes('json')); + $this->assertSame([Entrypoint::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); + } + + public function testNormalizeWithResourceMetadata(): void + { + $collection = new ResourceNameCollection([FooDummy::class, Dummy::class]); + $entrypoint = new Entrypoint($collection); + + $factoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $factoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ + (new ApiResource())->withShortName('Dummy')->withUriTemplate('/api/dummies')->withOperations(new Operations([ + 'get' => new GetCollection(), + ])), + ]))->shouldBeCalled(); + $factoryProphecy->create(FooDummy::class)->willReturn(new ResourceMetadataCollection(FooDummy::class, [ + (new ApiResource())->withShortName('FooDummy')->withUriTemplate('/api/foo_dummies')->withOperations(new Operations([ + 'get' => new GetCollection(), + ])), + ]))->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, Argument::any())->willReturn('/api/dummies')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource(FooDummy::class, UrlGeneratorInterface::ABS_PATH, Argument::any())->willReturn('/api/foo_dummies')->shouldBeCalled(); + + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + $urlGeneratorProphecy->generate('api_entrypoint')->willReturn('/api')->shouldBeCalled(); + $urlGeneratorProphecy->generate('api_jsonld_context', ['shortName' => 'Entrypoint'])->willReturn('/context/Entrypoint')->shouldBeCalled(); + + $normalizer = new EntrypointNormalizer($factoryProphecy->reveal(), $iriConverterProphecy->reveal(), $urlGeneratorProphecy->reveal()); + + $expected = [ + '@context' => '/context/Entrypoint', + '@id' => '/api', + '@type' => 'Entrypoint', + 'dummy' => '/api/dummies', + 'fooDummy' => '/api/foo_dummies', + ]; + $this->assertSame($expected, $normalizer->normalize($entrypoint, EntrypointNormalizer::FORMAT)); + } + + public function testNormalizeWithResourceCollection(): void + { + $collection = new ResourceNameCollection([FooDummy::class, Dummy::class]); + $entrypoint = new Entrypoint($collection); + + $factoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $factoryProphecy->create(Dummy::class)->willReturn( + new ResourceMetadataCollection(Dummy::class, [ + (new ApiResource())->withUriTemplate('Dummy')->withShortName('dummy')->withOperations(new Operations(['get' => (new GetCollection())])), + ]) + )->shouldBeCalled(); + + $factoryProphecy->create(FooDummy::class)->willReturn( + new ResourceMetadataCollection(FooDummy::class, [ + (new ApiResource())->withUriTemplate('FooDummy')->withShortName('fooDummy')->withOperations(new Operations(['get' => (new GetCollection())])), + ]) + )->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, Argument::any())->willReturn('/api/dummies')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource(FooDummy::class, UrlGeneratorInterface::ABS_PATH, Argument::any())->willReturn('/api/foo_dummies')->shouldBeCalled(); + + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + $urlGeneratorProphecy->generate('api_entrypoint')->willReturn('/api')->shouldBeCalled(); + $urlGeneratorProphecy->generate('api_jsonld_context', ['shortName' => 'Entrypoint'])->willReturn('/context/Entrypoint')->shouldBeCalled(); + + $normalizer = new EntrypointNormalizer($factoryProphecy->reveal(), $iriConverterProphecy->reveal(), $urlGeneratorProphecy->reveal()); + + $expected = [ + '@context' => '/context/Entrypoint', + '@id' => '/api', + '@type' => 'Entrypoint', + 'dummy' => '/api/dummies', + 'fooDummy' => '/api/foo_dummies', + ]; + $this->assertSame($expected, $normalizer->normalize($entrypoint, EntrypointNormalizer::FORMAT)); + } +} diff --git a/tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php b/src/Hydra/Tests/Serializer/PartialCollectionViewNormalizerTest.php similarity index 97% rename from tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php rename to src/Hydra/Tests/Serializer/PartialCollectionViewNormalizerTest.php index 8f60b7868ad..2b13ef703df 100644 --- a/tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/PartialCollectionViewNormalizerTest.php @@ -11,9 +11,10 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\Serializer; +namespace ApiPlatform\Hydra\Tests\Serializer; use ApiPlatform\Hydra\Serializer\PartialCollectionViewNormalizer; +use ApiPlatform\Hydra\Tests\Fixtures\Entity\SoMany; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Operations; @@ -21,7 +22,6 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; -use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SoMany; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -111,7 +111,10 @@ public function testNormalizeWithCursorBasedPagination(): void private function normalizePaginator(bool $partial = false, bool $cursor = false) { $paginatorProphecy = $this->prophesize($partial ? PartialPaginatorInterface::class : PaginatorInterface::class); - $paginatorProphecy->getCurrentPage()->willReturn(3)->shouldBeCalled(); + + if (!$cursor) { + $paginatorProphecy->getCurrentPage()->willReturn(3)->shouldBeCalled(); + } $decoratedNormalize = ['foo' => 'bar']; @@ -158,9 +161,6 @@ private function normalizePaginator(bool $partial = false, bool $cursor = false) return $normalizer->normalize($paginatorProphecy->reveal(), null, ['resource_class' => SoMany::class, 'operation_name' => 'get']); } - /** - * @group legacy - */ public function testSupportsNormalization(): void { $decoratedNormalizerProphecy = $this->prophesize(NormalizerInterface::class); diff --git a/tests/Hydra/State/HydraLinkProcessorTest.php b/src/Hydra/Tests/State/HydraLinkProcessorTest.php similarity index 97% rename from tests/Hydra/State/HydraLinkProcessorTest.php rename to src/Hydra/Tests/State/HydraLinkProcessorTest.php index 299d1560e0e..b5ce10a1009 100644 --- a/tests/Hydra/State/HydraLinkProcessorTest.php +++ b/src/Hydra/Tests/State/HydraLinkProcessorTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Hydra\State; +namespace ApiPlatform\Hydra\Tests\State; use ApiPlatform\Hydra\State\HydraLinkProcessor; use ApiPlatform\JsonLd\ContextBuilder; diff --git a/src/Hydra/composer.json b/src/Hydra/composer.json index 456a487c961..546d8238d8a 100644 --- a/src/Hydra/composer.json +++ b/src/Hydra/composer.json @@ -8,10 +8,7 @@ "API", "JSON-LD", "Hydra", - "JSONAPI", - "OpenAPI", - "HAL", - "Swagger" + "JSONAPI" ], "homepage": "https://api-platform.com/", "license": "MIT", @@ -27,18 +24,23 @@ } ], "require": { - "php": ">=8.1", - "api-platform/state": "*@dev || ^3.1", - "api-platform/documentation": "*@dev || ^3.1", - "api-platform/metadata": "*@dev || ^3.1", - "api-platform/jsonld": "*@dev || ^3.1", - "api-platform/json-schema": "*@dev || ^3.1", - "api-platform/serializer": "*@dev || ^3.1" + "php": ">=8.2", + "api-platform/state": "^4.1.8", + "api-platform/documentation": "^4.1", + "api-platform/metadata": "^4.2@beta", + "api-platform/jsonld": "^4.1", + "api-platform/json-schema": "^4.2@beta", + "api-platform/serializer": "^4.1", + "symfony/web-link": "^6.4 || ^7.1", + "symfony/type-info": "^7.3" }, "require-dev": { - "api-platform/doctrine-odm": "*@dev || ^3.2", - "api-platform/doctrine-orm": "*@dev || ^3.2", - "api-platform/doctrine-common": "*@dev || ^3.2" + "api-platform/doctrine-odm": "^4.1", + "api-platform/doctrine-orm": "^4.1", + "api-platform/doctrine-common": "^4.1", + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "11.5.x-dev" }, "autoload": { "psr-4": { @@ -60,13 +62,26 @@ }, "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" }, "symfony": { - "require": "^6.1" + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" } }, "scripts": { "test": "./vendor/bin/phpunit" - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] } diff --git a/src/Hydra/phpunit.baseline.xml b/src/Hydra/phpunit.baseline.xml new file mode 100644 index 00000000000..e3ef6196399 --- /dev/null +++ b/src/Hydra/phpunit.baseline.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/Hydra/phpunit.xml.dist b/src/Hydra/phpunit.xml.dist new file mode 100644 index 00000000000..1255627f373 --- /dev/null +++ b/src/Hydra/phpunit.xml.dist @@ -0,0 +1,23 @@ + + + + + + + + ./Tests/ + + + + + trigger_deprecation + + + ./ + + + ./Tests + ./vendor + + + diff --git a/src/JsonApi/.gitattributes b/src/JsonApi/.gitattributes new file mode 100644 index 00000000000..801f2080d71 --- /dev/null +++ b/src/JsonApi/.gitattributes @@ -0,0 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/JsonApi/.github/workflows/close_pr.yml b/src/JsonApi/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/JsonApi/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/JsonApi/.gitignore b/src/JsonApi/.gitignore new file mode 100644 index 00000000000..eb0a8e7b262 --- /dev/null +++ b/src/JsonApi/.gitignore @@ -0,0 +1,3 @@ +/composer.lock +/vendor +/.phpunit.result.cache diff --git a/src/JsonApi/Filter/SparseFieldset.php b/src/JsonApi/Filter/SparseFieldset.php new file mode 100644 index 00000000000..b937ea1ae73 --- /dev/null +++ b/src/JsonApi/Filter/SparseFieldset.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Filter; + +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Parameter as MetadataParameter; +use ApiPlatform\Metadata\ParameterProviderFilterInterface; +use ApiPlatform\Metadata\PropertiesAwareInterface; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter; + +final class SparseFieldset implements OpenApiParameterFilterInterface, JsonSchemaFilterInterface, ParameterProviderFilterInterface, PropertiesAwareInterface +{ + public function getSchema(MetadataParameter $parameter): array + { + return [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + ], + ]; + } + + public function getOpenApiParameters(MetadataParameter $parameter): Parameter + { + return new Parameter( + name: ($k = $parameter->getKey()).'[]', + in: $parameter instanceof QueryParameter ? 'query' : 'header', + description: 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: '.\sprintf( + '%1$s[]={propertyName}&%1$s[]={anotherPropertyName}', + $k + ) + ); + } + + public static function getParameterProvider(): string + { + return SparseFieldsetParameterProvider::class; + } +} diff --git a/src/JsonApi/Filter/SparseFieldsetParameterProvider.php b/src/JsonApi/Filter/SparseFieldsetParameterProvider.php new file mode 100644 index 00000000000..7ded8e9e765 --- /dev/null +++ b/src/JsonApi/Filter/SparseFieldsetParameterProvider.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Filter; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\State\ParameterProviderInterface; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + +final readonly class SparseFieldsetParameterProvider implements ParameterProviderInterface +{ + public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation + { + if (!($operation = $context['operation'] ?? null)) { + return null; + } + + $allowedProperties = $parameter->getExtraProperties()['_properties'] ?? []; + $value = $parameter->getValue(); + $normalizationContext = $operation->getNormalizationContext(); + + if (!\is_array($value)) { + return null; + } + + $properties = []; + $shortName = strtolower($operation->getShortName()); + foreach ($value as $resource => $fields) { + if (strtolower($resource) === $shortName) { + $p = &$properties; + } else { + $properties[$resource] = []; + $p = &$properties[$resource]; + } + + foreach (explode(',', $fields) as $f) { + if (\array_key_exists($f, $allowedProperties)) { + $p[] = $f; + } + } + } + + if (isset($normalizationContext[AbstractNormalizer::ATTRIBUTES])) { + $properties = array_merge_recursive((array) $normalizationContext[AbstractNormalizer::ATTRIBUTES], $properties); + } + + $normalizationContext[AbstractNormalizer::ATTRIBUTES] = $properties; + + return $operation->withNormalizationContext($normalizationContext); + } +} diff --git a/src/JsonApi/JsonSchema/SchemaFactory.php b/src/JsonApi/JsonSchema/SchemaFactory.php index e40ff6a2868..1189c34888d 100644 --- a/src/JsonApi/JsonSchema/SchemaFactory.php +++ b/src/JsonApi/JsonSchema/SchemaFactory.php @@ -13,16 +13,23 @@ namespace ApiPlatform\JsonApi\JsonSchema; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; +use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface; use ApiPlatform\JsonSchema\ResourceMetadataTrait; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryAwareInterface; use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\JsonSchema\SchemaUriPrefixTrait; use ApiPlatform\Metadata\Operation; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\TypeHelper; +use ApiPlatform\State\ApiResource\Error; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; +use Symfony\Component\TypeInfo\Type\ObjectType; /** * Decorator factory which adds JSON:API properties to the JSON Schema document. @@ -32,6 +39,17 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface { use ResourceMetadataTrait; + use SchemaUriPrefixTrait; + + /** + * As JSON:API recommends using [includes](https://jsonapi.org/format/#fetching-includes) instead of groups + * this flag allows to force using groups to generate the JSON:API JSONSchema. Defaults to true, use it in + * a serializer context. + */ + public const DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS = 'disable_json_schema_serializer_groups'; + + private const COLLECTION_BASE_SCHEMA_NAME = 'JsonApiCollectionBaseSchema'; + private const LINKS_PROPS = [ 'type' => 'object', 'properties' => [ @@ -106,8 +124,16 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareI ], ]; - public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private readonly ?DefinitionNameFactoryInterface $definitionNameFactory = null) + /** + * @var array + */ + private $builtSchema = []; + + public function __construct(private readonly SchemaFactoryInterface $schemaFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null) { + if (!$definitionNameFactory) { + $this->definitionNameFactory = new DefinitionNameFactory(); + } if ($this->schemaFactory instanceof SchemaFactoryAwareInterface) { $this->schemaFactory->setSchemaFactory($this); } @@ -123,47 +149,105 @@ public function buildSchema(string $className, string $format = 'jsonapi', strin if ('jsonapi' !== $format) { return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); } + + if (!$this->isResourceClass($className)) { + $operation = null; + $inputOrOutputClass = null; + $serializerContext ??= []; + } else { + $operation = $this->findOperation($className, $type, $operation, $serializerContext, $format); + $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext); + $serializerContext ??= $this->getSerializerContext($operation, $type); + } + + if (null === $inputOrOutputClass) { + // input or output disabled + return $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, $serializerContext, $forceCollection); + } + // We don't use the serializer context here as JSON:API doesn't leverage serializer groups for related resources. // That is done by query parameter. @see https://jsonapi.org/format/#fetching-includes - $schema = $this->schemaFactory->buildSchema($className, $format, $type, $operation, $schema, [], $forceCollection); - - if (($key = $schema->getRootDefinitionKey()) || ($key = $schema->getItemsDefinitionKey())) { - $definitions = $schema->getDefinitions(); - $properties = $definitions[$key]['properties'] ?? []; + $jsonApiSerializerContext = $serializerContext; + if (true === ($serializerContext[self::DISABLE_JSON_SCHEMA_SERIALIZER_GROUPS] ?? true) && $inputOrOutputClass === $className) { + unset($jsonApiSerializerContext['groups']); + } - // Prevent reapplying - if (isset($properties['id'], $properties['type']) || isset($properties['data'])) { - return $schema; - } + $schema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, $schema, $jsonApiSerializerContext, $forceCollection); + $definitionName = $this->definitionNameFactory->create($inputOrOutputClass, $format, $className, $operation, $jsonApiSerializerContext); + $prefix = $this->getSchemaUriPrefix($schema->getVersion()); + $definitions = $schema->getDefinitions(); + $collectionKey = $schema->getItemsDefinitionKey(); - $definitions[$key]['properties'] = $this->buildDefinitionPropertiesSchema($key, $className, $format, $type, $operation, $schema, []); + // Already computed + if (!$collectionKey && isset($definitions[$definitionName])) { + $schema['$ref'] = $prefix.$definitionName; - if ($schema->getRootDefinitionKey()) { - return $schema; - } + return $schema; } - if (($schema['type'] ?? '') === 'array') { - // data - $items = $schema['items']; - unset($schema['items']); + $key = $schema->getRootDefinitionKey() ?? $collectionKey; + $properties = $definitions[$definitionName]['properties'] ?? []; - $schema['type'] = 'object'; - $schema['properties'] = [ - 'links' => self::LINKS_PROPS, - 'meta' => self::META_PROPS, - 'data' => [ + if (Error::class === $className && !isset($properties['errors'])) { + $definitions[$definitionName]['properties'] = [ + 'errors' => [ 'type' => 'array', - 'items' => $items, + 'items' => [ + 'allOf' => [ + ['$ref' => $prefix.$key], + ['type' => 'object', 'properties' => ['source' => ['type' => 'object'], 'status' => ['type' => 'string']]], + ], + ], ], ]; - $schema['required'] = [ - 'data', - ]; + $schema['$ref'] = $prefix.$definitionName; + + return $schema; + } + + if (!$collectionKey) { + $definitions[$definitionName]['properties'] = $this->buildDefinitionPropertiesSchema($key, $className, $format, $type, $operation, $schema, []); + $schema['$ref'] = $prefix.$definitionName; + + return $schema; + } + + if (($schema['type'] ?? '') !== 'array') { return $schema; } + if (!isset($definitions[self::COLLECTION_BASE_SCHEMA_NAME])) { + $definitions[self::COLLECTION_BASE_SCHEMA_NAME] = [ + 'type' => 'object', + 'properties' => [ + 'links' => self::LINKS_PROPS, + 'meta' => self::META_PROPS, + 'data' => [ + 'type' => 'array', + ], + ], + 'required' => ['data'], + ]; + } + + unset($schema['items']); + unset($schema['type']); + + $properties = $this->buildDefinitionPropertiesSchema($key, $className, $format, $type, $operation, $schema, []); + $properties['data']['properties']['attributes']['$ref'] = $prefix.$key; + + $schema['description'] = "$definitionName collection."; + $schema['allOf'] = [ + ['$ref' => $prefix.self::COLLECTION_BASE_SCHEMA_NAME], + ['type' => 'object', 'properties' => [ + 'data' => [ + 'type' => 'array', + 'items' => $properties['data'], + ], + ]], + ]; + return $schema; } @@ -191,12 +275,27 @@ private function buildDefinitionPropertiesSchema(string $key, string $className, continue; } - $operation = $this->findOperation($relatedClassName, $type, $operation, $serializerContext); + $operation = $this->findOperation($relatedClassName, $type, null, $serializerContext); $inputOrOutputClass = $this->findOutputClass($relatedClassName, $type, $operation, $serializerContext); $serializerContext ??= $this->getSerializerContext($operation, $type); $definitionName = $this->definitionNameFactory->create($relatedClassName, $format, $inputOrOutputClass, $operation, $serializerContext); - $ref = Schema::VERSION_OPENAPI === $schema->getVersion() ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName; - $refs[$ref] = '$ref'; + + // to avoid recursion + if ($this->builtSchema[$definitionName] ?? false) { + $refs[$this->getSchemaUriPrefix($schema->getVersion()).$definitionName] = '$ref'; + continue; + } + + if (!isset($definitions[$definitionName])) { + $this->builtSchema[$definitionName] = true; + $subSchema = new Schema($schema->getVersion()); + $subSchema->setDefinitions($schema->getDefinitions()); + $subSchema = $this->buildSchema($relatedClassName, $format, $type, $operation, $subSchema, $serializerContext + [self::FORCE_SUBSCHEMA => true], false); + $schema->setDefinitions($subSchema->getDefinitions()); + $definitions = $schema->getDefinitions(); + } + + $refs[$this->getSchemaUriPrefix($schema->getVersion()).$definitionName] = '$ref'; } $relatedDefinitions[$propertyName] = array_flip($refs); if ($isOne) { @@ -209,15 +308,18 @@ private function buildDefinitionPropertiesSchema(string $key, string $className, ]; continue; } + if ('id' === $propertyName) { + // should probably be renamed "lid" and moved to the above node $attributes['_id'] = $property; continue; } $attributes[$propertyName] = $property; } + $currentRef = $this->getSchemaUriPrefix($schema->getVersion()).$schema->getRootDefinitionKey(); $replacement = self::PROPERTY_PROPS; - $replacement['attributes']['properties'] = $attributes; + $replacement['attributes'] = ['$ref' => $currentRef]; $included = []; if (\count($relationships) > 0) { @@ -240,19 +342,6 @@ private function buildDefinitionPropertiesSchema(string $key, string $className, ]; } - if ($required = $definitions[$key]['required'] ?? null) { - foreach ($required as $require) { - if (isset($replacement['attributes']['properties'][$require])) { - $replacement['attributes']['required'][] = $require; - continue; - } - if (isset($relationships[$require])) { - $replacement['relationships']['required'][] = $require; - } - } - unset($definitions[$key]['required']); - } - return [ 'data' => [ 'type' => 'object', @@ -265,21 +354,60 @@ private function buildDefinitionPropertiesSchema(string $key, string $className, private function getRelationship(string $resourceClass, string $property, ?array $serializerContext): ?array { $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property, $serializerContext ?? []); - $types = $propertyMetadata->getBuiltinTypes() ?? []; + + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $types = $propertyMetadata->getBuiltinTypes() ?? []; + $isRelationship = false; + $isOne = $isMany = false; + $relatedClasses = []; + + foreach ($types as $type) { + if ($type->isCollection()) { + $collectionValueType = $type->getCollectionValueTypes()[0] ?? null; + $isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className); + } else { + $isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className); + } + if (!isset($className) || (!$isOne && !$isMany)) { + continue; + } + $isRelationship = true; + $resourceMetadata = $this->resourceMetadataFactory->create($className); + $operation = $resourceMetadata->getOperation(); + // @see https://github.com/api-platform/core/issues/5501 + // @see https://github.com/api-platform/core/pull/5722 + $relatedClasses[$className] = $operation->canRead(); + } + + return $isRelationship ? [$isOne, $relatedClasses] : null; + } + + if (null === $type = $propertyMetadata->getNativeType()) { + return null; + } + $isRelationship = false; $isOne = $isMany = false; $relatedClasses = []; - foreach ($types as $type) { - if ($type->isCollection()) { - $collectionValueType = $type->getCollectionValueTypes()[0] ?? null; - $isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className); - } else { - $isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className); + /** @var class-string|null $className */ + $className = null; + + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); + }; + + foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) { + if (TypeHelper::getCollectionValueType($t)?->isSatisfiedBy($typeIsResourceClass)) { + $isMany = true; + } elseif ($t->isSatisfiedBy($typeIsResourceClass)) { + $isOne = true; } - if (!isset($className) || (!$isOne && !$isMany)) { + + if (!$className || (!$isOne && !$isMany)) { continue; } + $isRelationship = true; $resourceMetadata = $this->resourceMetadataFactory->create($className); $operation = $resourceMetadata->getOperation(); diff --git a/src/JsonApi/README.md b/src/JsonApi/README.md new file mode 100644 index 00000000000..ab44d8430f7 --- /dev/null +++ b/src/JsonApi/README.md @@ -0,0 +1,14 @@ +# API Platform - JSON:API + +The [JSON:API](https://jsonapi.org/) component of the [API Platform](https://api-platform.com) framework. + +JSON:API is a popular specification for building APIs in JSON. + +[Documentation](https://api-platform.com/docs/core/content-negotiation/) + +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/JsonApi/Serializer/CollectionNormalizer.php b/src/JsonApi/Serializer/CollectionNormalizer.php index 8fe2be7c32b..1c1f362ce0d 100644 --- a/src/JsonApi/Serializer/CollectionNormalizer.php +++ b/src/JsonApi/Serializer/CollectionNormalizer.php @@ -13,11 +13,10 @@ namespace ApiPlatform\JsonApi\Serializer; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\IriHelper; use ApiPlatform\Serializer\AbstractCollectionNormalizer; -use ApiPlatform\Util\IriHelper; use Symfony\Component\Serializer\Exception\UnexpectedValueException; /** @@ -31,7 +30,7 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer { public const FORMAT = 'jsonapi'; - public function __construct(ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) + public function __construct(ResourceClassResolverInterface $resourceClassResolver, string $pageParameterName, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) { parent::__construct($resourceClassResolver, $pageParameterName, $resourceMetadataFactory); } @@ -39,7 +38,7 @@ public function __construct(ResourceClassResolverInterface|LegacyResourceClassRe /** * {@inheritdoc} */ - protected function getPaginationData($object, array $context = []): array + protected function getPaginationData(iterable $object, array $context = []): array { [$paginator, $paginated, $currentPage, $itemsPerPage, $lastPage, $pageTotalItems, $totalItems] = $this->getPaginationConfig($object, $context); $parsed = IriHelper::parseIri($context['uri'] ?? '/', $this->pageParameterName); @@ -85,7 +84,7 @@ protected function getPaginationData($object, array $context = []): array * * @throws UnexpectedValueException */ - protected function getItemsData($object, ?string $format = null, array $context = []): array + protected function getItemsData(iterable $object, ?string $format = null, array $context = []): array { $data = [ 'data' => [], diff --git a/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php b/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php index 92afba9744f..fc87afcae7c 100644 --- a/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php +++ b/src/JsonApi/Serializer/ConstraintViolationListNormalizer.php @@ -14,10 +14,10 @@ namespace ApiPlatform\JsonApi\Serializer; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; +use Symfony\Component\TypeInfo\Type\ObjectType; use Symfony\Component\Validator\ConstraintViolationInterface; use Symfony\Component\Validator\ConstraintViolationListInterface; @@ -26,7 +26,7 @@ * * @author Héctor Hurtarte */ -final class ConstraintViolationListNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ConstraintViolationListNormalizer implements NormalizerInterface { public const FORMAT = 'jsonapi'; @@ -60,25 +60,14 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && $data instanceof ConstraintViolationListInterface; } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { return self::FORMAT === $format ? [ConstraintViolationListInterface::class => true] : []; } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } - private function getSourcePointerFromViolation(ConstraintViolationInterface $violation): string { $fieldName = $violation->getPropertyPath(); @@ -87,7 +76,13 @@ private function getSourcePointerFromViolation(ConstraintViolationInterface $vio return 'data'; } - $class = $violation->getRoot()::class; + $root = $violation->getRoot(); + + if (!\is_object($root)) { + return "data/attributes/$fieldName"; + } + + $class = $root::class; $propertyMetadata = $this->propertyMetadataFactory ->create( // Im quite sure this requires some thought in case of validations over relationships @@ -99,9 +94,15 @@ private function getSourcePointerFromViolation(ConstraintViolationInterface $vio $fieldName = $this->nameConverter->normalize($fieldName, $class, self::FORMAT); } - $type = $propertyMetadata->getBuiltinTypes()[0] ?? null; - if ($type && null !== $type->getClassName()) { - return "data/relationships/$fieldName"; + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $type = $propertyMetadata->getBuiltinTypes()[0] ?? null; + if ($type && null !== $type->getClassName()) { + return "data/relationships/$fieldName"; + } + } else { + if ($propertyMetadata->getNativeType()?->isSatisfiedBy(fn ($t) => $t instanceof ObjectType)) { + return "data/relationships/$fieldName"; + } } return "data/attributes/$fieldName"; diff --git a/src/JsonApi/Serializer/EntrypointNormalizer.php b/src/JsonApi/Serializer/EntrypointNormalizer.php index 5414d4da658..394bcb3cc39 100644 --- a/src/JsonApi/Serializer/EntrypointNormalizer.php +++ b/src/JsonApi/Serializer/EntrypointNormalizer.php @@ -13,19 +13,14 @@ namespace ApiPlatform\JsonApi\Serializer; -use ApiPlatform\Api\Entrypoint; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface as LegacyUrlGeneratorInterface; -use ApiPlatform\Documentation\Entrypoint as DocumentationEntrypoint; -use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Documentation\Entrypoint; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Normalizes the API entrypoint. @@ -33,18 +28,18 @@ * @author Amrouche Hamza * @author Kévin Dunglas */ -final class EntrypointNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class EntrypointNormalizer implements NormalizerInterface { public const FORMAT = 'jsonapi'; - public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly UrlGeneratorInterface|LegacyUrlGeneratorInterface $urlGenerator) + public function __construct(private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly IriConverterInterface $iriConverter, private readonly UrlGeneratorInterface $urlGenerator) { } /** * {@inheritdoc} */ - public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + public function normalize(mixed $object, ?string $format = null, array $context = []): array { $entrypoint = ['links' => ['self' => $this->urlGenerator->generate('api_entrypoint', [], UrlGeneratorInterface::ABS_URL)]]; @@ -58,7 +53,7 @@ public function normalize(mixed $object, ?string $format = null, array $context } try { - $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_URL, $operation); // @phpstan-ignore-line phpstan issue as type is CollectionOperationInterface & Operation + $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_URL, $operation); $entrypoint['links'][lcfirst($resource->getShortName())] = $iri; } catch (InvalidArgumentException) { // Ignore resources without GET operations @@ -75,25 +70,14 @@ public function normalize(mixed $object, ?string $format = null, array $context */ public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { - return self::FORMAT === $format && ($data instanceof Entrypoint || $data instanceof DocumentationEntrypoint); + return self::FORMAT === $format && $data instanceof Entrypoint; } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { - return self::FORMAT === $format ? [Entrypoint::class => true, DocumentationEntrypoint::class => true] : []; - } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; + return self::FORMAT === $format ? [Entrypoint::class => true] : []; } } diff --git a/src/JsonApi/Serializer/ErrorNormalizer.php b/src/JsonApi/Serializer/ErrorNormalizer.php index 19764afa3bf..a9eba146f8f 100644 --- a/src/JsonApi/Serializer/ErrorNormalizer.php +++ b/src/JsonApi/Serializer/ErrorNormalizer.php @@ -13,32 +13,20 @@ namespace ApiPlatform\JsonApi\Serializer; -use ApiPlatform\Problem\Serializer\ErrorNormalizerTrait; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use ApiPlatform\Symfony\Validator\Exception\ConstraintViolationListAwareExceptionInterface as LegacyConstraintViolationListAwareExceptionInterface; -use ApiPlatform\Validator\Exception\ConstraintViolationListAwareExceptionInterface; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Converts {@see \Exception} or {@see FlattenException} or to a JSON API error representation. * * @author Héctor Hurtarte */ -final class ErrorNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ErrorNormalizer implements NormalizerInterface { - use ErrorNormalizerTrait; - public const FORMAT = 'jsonapi'; - public const TITLE = 'title'; - private array $defaultContext = [ - self::TITLE => 'An error occurred', - ]; - public function __construct(private readonly bool $debug = false, array $defaultContext = [], private ?NormalizerInterface $itemNormalizer = null, private ?NormalizerInterface $constraintViolationListNormalizer = null) + public function __construct(private ?NormalizerInterface $itemNormalizer = null) { - $this->defaultContext = array_merge($this->defaultContext, $defaultContext); } /** @@ -46,35 +34,37 @@ public function __construct(private readonly bool $debug = false, array $default */ public function normalize(mixed $object, ?string $format = null, array $context = []): array { - // TODO: in api platform 4 this will be the default, note that JSON:API is close to Problem so we should use the same normalizer - if ($context['rfc_7807_compliant_errors'] ?? false) { - if ($object instanceof LegacyConstraintViolationListAwareExceptionInterface || $object instanceof ConstraintViolationListAwareExceptionInterface) { - // TODO: return ['errors' => $this->constraintViolationListNormalizer(...)] - return $this->constraintViolationListNormalizer->normalize($object->getConstraintViolationList(), $format, $context); - } - - $jsonApiObject = $this->itemNormalizer->normalize($object, $format, $context); - $error = $jsonApiObject['data']['attributes']; - $error['id'] = $jsonApiObject['data']['id']; - $error['type'] = $jsonApiObject['data']['id']; - - return ['errors' => [$error]]; + $jsonApiObject = $this->itemNormalizer->normalize($object, $format, $context); + $error = $jsonApiObject['data']['attributes']; + $error['id'] = $jsonApiObject['data']['id']; + if (isset($error['type'])) { + $error['links'] = ['type' => $error['type']]; } - $data = [ - 'title' => $context[self::TITLE] ?? $this->defaultContext[self::TITLE], - 'description' => $this->getErrorMessage($object, $context, $this->debug), - ]; + if (!isset($error['code']) && method_exists($object, 'getId')) { + $error['code'] = $object->getId(); + } - if (null !== $errorCode = $this->getErrorCode($object)) { - $data['code'] = $errorCode; + if (!isset($error['violations'])) { + return ['errors' => [$error]]; } - if ($this->debug && null !== $trace = $object->getTrace()) { - $data['trace'] = $trace; + $errors = []; + foreach ($error['violations'] as $violation) { + $e = ['detail' => $violation['message']] + $error; + if (isset($error['links']['type'])) { + $type = $error['links']['type']; + $e['links']['type'] = \sprintf('%s/%s', $type, $violation['propertyPath']); + $e['id'] = str_replace($type, $e['links']['type'], $e['id']); + } + if (isset($e['code'])) { + $e['code'] = \sprintf('%s/%s', $error['code'], $violation['propertyPath']); + } + unset($e['violations']); + $errors[] = $e; } - return $data; + return ['errors' => $errors]; } /** @@ -85,6 +75,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && ($data instanceof \Exception || $data instanceof FlattenException); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { if (self::FORMAT === $format) { @@ -96,18 +89,4 @@ public function getSupportedTypes($format): array return []; } - - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return true; - } } diff --git a/src/JsonApi/Serializer/ItemNormalizer.php b/src/JsonApi/Serializer/ItemNormalizer.php index c928d56fc90..47a5879a4b4 100644 --- a/src/JsonApi/Serializer/ItemNormalizer.php +++ b/src/JsonApi/Serializer/ItemNormalizer.php @@ -13,24 +13,24 @@ namespace ApiPlatform\JsonApi\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; -use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Exception\ItemNotFoundException; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; +use ApiPlatform\Metadata\Util\TypeHelper; use ApiPlatform\Serializer\AbstractItemNormalizer; use ApiPlatform\Serializer\CacheKeyTrait; use ApiPlatform\Serializer\ContextTrait; use ApiPlatform\Serializer\TagCollectorInterface; -use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\RuntimeException; @@ -38,6 +38,9 @@ use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; +use Symfony\Component\TypeInfo\Type\ObjectType; /** * Converts between objects and array. @@ -56,7 +59,7 @@ final class ItemNormalizer extends AbstractItemNormalizer private array $componentsCache = []; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface|LegacyIriConverterInterface $iriConverter, ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) { parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector); } @@ -69,6 +72,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context) && !($data instanceof \Exception || $data instanceof FlattenException); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { return self::FORMAT === $format ? parent::getSupportedTypes($format) : []; @@ -94,8 +100,8 @@ public function normalize(mixed $object, ?string $format = null, array $context } $context = $this->initContext($resourceClass, $context); - $iri = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context); - $context['iri'] = $iri; + + $iri = $context['iri'] ??= $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context); $context['object'] = $object; $context['format'] = $format; $context['api_normalize'] = true; @@ -321,50 +327,101 @@ private function getComponents(object $object, ?string $format, array $context): ->propertyMetadataFactory ->create($context['resource_class'], $attribute, $options); - $types = $propertyMetadata->getBuiltinTypes() ?? []; - // prevent declaring $attribute as attribute if it's already declared as relationship $isRelationship = false; - foreach ($types as $type) { - $isOne = $isMany = false; + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $types = $propertyMetadata->getBuiltinTypes() ?? []; - if ($type->isCollection()) { - $collectionValueType = $type->getCollectionValueTypes()[0] ?? null; - $isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className); - } else { - $isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className); - } + foreach ($types as $type) { + $isOne = $isMany = false; - if (!isset($className) || !$isOne && !$isMany) { - // don't declare it as an attribute too quick: maybe the next type is a valid resource - continue; - } + if ($type->isCollection()) { + $collectionValueType = $type->getCollectionValueTypes()[0] ?? null; + $isMany = $collectionValueType && ($className = $collectionValueType->getClassName()) && $this->resourceClassResolver->isResourceClass($className); + } else { + $isOne = ($className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className); + } + + if (!isset($className) || !$isOne && !$isMany) { + // don't declare it as an attribute too quick: maybe the next type is a valid resource + continue; + } - $relation = [ - 'name' => $attribute, - 'type' => $this->getResourceShortName($className), - 'cardinality' => $isOne ? 'one' : 'many', - ]; - - // if we specify the uriTemplate, generates its value for link definition - // @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content - if ($itemUriTemplate = $propertyMetadata->getUriTemplate()) { - $attributeValue = $this->propertyAccessor->getValue($object, $attribute); - $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); - $childContext = $this->createChildContext($context, $attribute, $format); - unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']); - - $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation( - operationName: $itemUriTemplate, - httpOperation: true - ); - - $components['links'][$attribute] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext); + $relation = [ + 'name' => $attribute, + 'type' => $this->getResourceShortName($className), + 'cardinality' => $isOne ? 'one' : 'many', + ]; + + // if we specify the uriTemplate, generates its value for link definition + // @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content + if ($itemUriTemplate = $propertyMetadata->getUriTemplate()) { + $attributeValue = $this->propertyAccessor->getValue($object, $attribute); + $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); + $childContext = $this->createChildContext($context, $attribute, $format); + unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']); + + $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation( + operationName: $itemUriTemplate, + httpOperation: true + ); + + $components['links'][$attribute] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext); + } + + $components['relationships'][] = $relation; + $isRelationship = true; } + } else { + if ($type = $propertyMetadata->getNativeType()) { + /** @var class-string|null $className */ + $className = null; + + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); + }; + + foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) { + $isOne = $isMany = false; + + if (TypeHelper::getCollectionValueType($t)?->isSatisfiedBy($typeIsResourceClass)) { + $isMany = true; + } elseif ($t->isSatisfiedBy($typeIsResourceClass)) { + $isOne = true; + } + + if (!$className || (!$isOne && !$isMany)) { + // don't declare it as an attribute too quick: maybe the next type is a valid resource + continue; + } - $components['relationships'][] = $relation; - $isRelationship = true; + $relation = [ + 'name' => $attribute, + 'type' => $this->getResourceShortName($className), + 'cardinality' => $isOne ? 'one' : 'many', + ]; + + // if we specify the uriTemplate, generates its value for link definition + // @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content + if ($itemUriTemplate = $propertyMetadata->getUriTemplate()) { + $attributeValue = $this->propertyAccessor->getValue($object, $attribute); + $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); + $childContext = $this->createChildContext($context, $attribute, $format); + unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']); + + $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation( + operationName: $itemUriTemplate, + httpOperation: true + ); + + $components['links'][$attribute] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext); + } + + $components['relationships'][] = $relation; + $isRelationship = true; + } + } } // if all types are not relationships, declare it as an attribute diff --git a/src/JsonApi/Serializer/ObjectNormalizer.php b/src/JsonApi/Serializer/ObjectNormalizer.php index 2dc6a4c9274..1fd986b2dbb 100644 --- a/src/JsonApi/Serializer/ObjectNormalizer.php +++ b/src/JsonApi/Serializer/ObjectNormalizer.php @@ -13,28 +13,23 @@ namespace ApiPlatform\JsonApi\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Decorates the output with JSON API metadata when appropriate, but otherwise * just passes through to the decorated normalizer. */ -final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ObjectNormalizer implements NormalizerInterface { use ClassInfoTrait; public const FORMAT = 'jsonapi'; - public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) + public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory) { } @@ -46,32 +41,14 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && $this->decorated->supportsNormalization($data, $format, $context); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->decorated, 'getSupportedTypes')) { - return [ - '*' => $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(), - ]; - } - return self::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : []; } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} */ diff --git a/src/JsonApi/Serializer/ReservedAttributeNameConverter.php b/src/JsonApi/Serializer/ReservedAttributeNameConverter.php index e25ad566dc9..f00977d7928 100644 --- a/src/JsonApi/Serializer/ReservedAttributeNameConverter.php +++ b/src/JsonApi/Serializer/ReservedAttributeNameConverter.php @@ -13,6 +13,7 @@ namespace ApiPlatform\JsonApi\Serializer; +use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; @@ -21,7 +22,7 @@ * * @author Baptiste Meyer */ -final class ReservedAttributeNameConverter implements AdvancedNameConverterInterface +final class ReservedAttributeNameConverter implements NameConverterInterface, AdvancedNameConverterInterface { public const JSON_API_RESERVED_ATTRIBUTES = [ 'id' => '_id', @@ -44,6 +45,10 @@ public function normalize(string $propertyName, ?string $class = null, ?string $ $propertyName = $this->nameConverter->normalize($propertyName, $class, $format, $context); } + if ($class && is_a($class, ProblemExceptionInterface::class, true)) { + return $propertyName; + } + if (isset(self::JSON_API_RESERVED_ATTRIBUTES[$propertyName])) { $propertyName = self::JSON_API_RESERVED_ATTRIBUTES[$propertyName]; } diff --git a/src/JsonApi/Tests/Fixtures/CircularReference.php b/src/JsonApi/Tests/Fixtures/CircularReference.php new file mode 100644 index 00000000000..d9808dd2f3e --- /dev/null +++ b/src/JsonApi/Tests/Fixtures/CircularReference.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Tests\Fixtures; + +class CircularReference +{ + public $id; + public CircularReference $parent; +} diff --git a/src/JsonApi/Tests/Fixtures/CustomConverter.php b/src/JsonApi/Tests/Fixtures/CustomConverter.php new file mode 100644 index 00000000000..2d046fe84a5 --- /dev/null +++ b/src/JsonApi/Tests/Fixtures/CustomConverter.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Tests\Fixtures; + +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +/** + * Custom converter that will only convert a property named "nameConverted" + * with the same logic as Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter. + */ +class CustomConverter implements NameConverterInterface +{ + private NameConverterInterface $nameConverter; + + public function __construct() + { + $this->nameConverter = new CamelCaseToSnakeCaseNameConverter(); + } + + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + return 'nameConverted' === $propertyName ? $this->nameConverter->normalize($propertyName) : $propertyName; + } + + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + return 'name_converted' === $propertyName ? $this->nameConverter->denormalize($propertyName) : $propertyName; + } +} diff --git a/src/JsonApi/Tests/Fixtures/Dummy.php b/src/JsonApi/Tests/Fixtures/Dummy.php new file mode 100644 index 00000000000..3f3b9e16b05 --- /dev/null +++ b/src/JsonApi/Tests/Fixtures/Dummy.php @@ -0,0 +1,206 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Tests\Fixtures; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Dummy. + * + * @author Kévin Dunglas + */ +#[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false])] +class Dummy +{ + /** + * @var int|null The id + */ + private $id; + + /** + * @var string The dummy name + */ + #[ApiProperty(iris: ['https://schema.org/name'])] + #[Assert\NotBlank] + private string $name; + + /** + * @var string|null The dummy name alias + */ + #[ApiProperty(iris: ['https://schema.org/alternateName'])] + private $alias; + + /** + * @var array foo + */ + private ?array $foo = null; + + /** + * @var string|null A short description of the item + */ + #[ApiProperty(iris: ['https://schema.org/description'])] + public $description; + + /** + * @var string|null A dummy + */ + public $dummy; + + /** + * @var bool|null A dummy boolean + */ + public ?bool $dummyBoolean = null; + /** + * @var \DateTime|null A dummy date + */ + #[ApiProperty(iris: ['https://schema.org/DateTime'])] + public $dummyDate; + + /** + * @var float|null A dummy float + */ + public $dummyFloat; + + /** + * @var string|null A dummy price + */ + public $dummyPrice; + + /** + * @var array|null serialize data + */ + public $jsonData = []; + + /** + * @var array|null + */ + public $arrayData = []; + + /** + * @var string|null + */ + public $nameConverted; + + public static function staticMethod(): void + { + } + + public function getId() + { + return $this->id; + } + + public function setId($id): void + { + $this->id = $id; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } + + public function setAlias($alias): void + { + $this->alias = $alias; + } + + public function getAlias() + { + return $this->alias; + } + + public function setDescription($description): void + { + $this->description = $description; + } + + public function getDescription() + { + return $this->description; + } + + public function fooBar($baz): void + { + } + + public function getFoo(): ?array + { + return $this->foo; + } + + public function setFoo(?array $foo = null): void + { + $this->foo = $foo; + } + + public function setDummyDate(?\DateTime $dummyDate = null): void + { + $this->dummyDate = $dummyDate; + } + + public function getDummyDate() + { + return $this->dummyDate; + } + + public function setDummyPrice($dummyPrice) + { + $this->dummyPrice = $dummyPrice; + + return $this; + } + + public function getDummyPrice() + { + return $this->dummyPrice; + } + + public function setJsonData($jsonData): void + { + $this->jsonData = $jsonData; + } + + public function getJsonData() + { + return $this->jsonData; + } + + public function setArrayData($arrayData): void + { + $this->arrayData = $arrayData; + } + + public function getArrayData() + { + return $this->arrayData; + } + + public function setDummy($dummy = null): void + { + $this->dummy = $dummy; + } + + public function getDummy() + { + return $this->dummy; + } +} diff --git a/tests/Mock/Exception/ErrorCodeSerializable.php b/src/JsonApi/Tests/Fixtures/ErrorCodeSerializable.php similarity index 81% rename from tests/Mock/Exception/ErrorCodeSerializable.php rename to src/JsonApi/Tests/Fixtures/ErrorCodeSerializable.php index 2531b33f207..eff84141639 100644 --- a/tests/Mock/Exception/ErrorCodeSerializable.php +++ b/src/JsonApi/Tests/Fixtures/ErrorCodeSerializable.php @@ -11,9 +11,9 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\Mock\Exception; +namespace ApiPlatform\JsonApi\Tests\Fixtures; -use ApiPlatform\Exception\ErrorCodeSerializableInterface; +use ApiPlatform\Metadata\Exception\ErrorCodeSerializableInterface; class ErrorCodeSerializable extends \Exception implements ErrorCodeSerializableInterface { diff --git a/src/JsonApi/Tests/Fixtures/RelatedDummy.php b/src/JsonApi/Tests/Fixtures/RelatedDummy.php new file mode 100644 index 00000000000..befb25d37c4 --- /dev/null +++ b/src/JsonApi/Tests/Fixtures/RelatedDummy.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Tests\Fixtures; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Related Dummy. + * + * @author Kévin Dunglas + */ +#[ApiResource] +class RelatedDummy implements \Stringable +{ + #[ApiProperty(writable: false)] + #[Groups(['chicago', 'friends'])] + private $id; + + /** + * @var string|null A name + */ + #[ApiProperty(iris: ['RelatedDummy.name'])] + #[Groups(['friends'])] + public $name; + + #[ApiProperty(deprecationReason: 'This property is deprecated for upgrade test')] + #[Groups(['barcelona', 'chicago', 'friends'])] + protected $symfony = 'symfony'; + + /** + * @var \DateTime|null A dummy date + */ + #[Groups(['friends'])] + public $dummyDate; + + /** + * @var bool|null A dummy bool + */ + #[Groups(['friends'])] + public ?bool $dummyBoolean = null; + + public function __construct() + { + } + + public function getId() + { + return $this->id; + } + + public function setId($id): void + { + $this->id = $id; + } + + public function setName($name): void + { + $this->name = $name; + } + + public function getName() + { + return $this->name; + } + + public function getSymfony() + { + return $this->symfony; + } + + public function setSymfony($symfony): void + { + $this->symfony = $symfony; + } + + public function setDummyDate(\DateTime $dummyDate): void + { + $this->dummyDate = $dummyDate; + } + + public function getDummyDate() + { + return $this->dummyDate; + } + + public function isDummyBoolean(): ?bool + { + return $this->dummyBoolean; + } + + /** + * @param bool $dummyBoolean + */ + public function setDummyBoolean($dummyBoolean): void + { + $this->dummyBoolean = $dummyBoolean; + } + + public function __toString(): string + { + return (string) $this->getId(); + } +} diff --git a/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php b/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php new file mode 100644 index 00000000000..28b9c3a5552 --- /dev/null +++ b/src/JsonApi/Tests/JsonSchema/SchemaFactoryTest.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Tests\JsonSchema; + +use ApiPlatform\JsonApi\JsonSchema\SchemaFactory; +use ApiPlatform\JsonApi\Tests\Fixtures\Dummy; +use ApiPlatform\JsonSchema\DefinitionNameFactory; +use ApiPlatform\JsonSchema\Schema; +use ApiPlatform\JsonSchema\SchemaFactory as BaseSchemaFactory; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; + +class SchemaFactoryTest extends TestCase +{ + use ProphecyTrait; + + private SchemaFactory $schemaFactory; + + protected function setUp(): void + { + $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactory->create(Dummy::class)->willReturn( + new ResourceMetadataCollection(Dummy::class, [ + (new ApiResource())->withOperations(new Operations([ + 'get' => (new Get())->withName('get'), + ])), + ]) + ); + $propertyNameCollectionFactory = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactory->create(Dummy::class, ['enable_getter_setter_extraction' => true, 'schema_type' => Schema::TYPE_OUTPUT])->willReturn(new PropertyNameCollection()); + $propertyMetadataFactory = $this->prophesize(PropertyMetadataFactoryInterface::class); + + $definitionNameFactory = new DefinitionNameFactory(null); + + $baseSchemaFactory = new BaseSchemaFactory( + resourceMetadataFactory: $resourceMetadataFactory->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactory->reveal(), + propertyMetadataFactory: $propertyMetadataFactory->reveal(), + definitionNameFactory: $definitionNameFactory, + ); + + $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolver->isResourceClass(Dummy::class)->willReturn(true); + + $this->schemaFactory = new SchemaFactory( + schemaFactory: $baseSchemaFactory, + propertyMetadataFactory: $propertyMetadataFactory->reveal(), + resourceClassResolver: $resourceClassResolver->reveal(), + resourceMetadataFactory: $resourceMetadataFactory->reveal(), + definitionNameFactory: $definitionNameFactory, + ); + } + + public function testBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class); + + $this->assertTrue($resultSchema->isDefined()); + $this->assertSame('Dummy.jsonapi', $resultSchema->getRootDefinitionKey()); + } + + public function testCustomFormatBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonapi'); + + $this->assertTrue($resultSchema->isDefined()); + $this->assertSame('Dummy.jsonapi', $resultSchema->getRootDefinitionKey()); + } + + public function testHasRootDefinitionKeyBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class); + $definitions = $resultSchema->getDefinitions(); + $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); + + $this->assertTrue(isset($definitions[$rootDefinitionKey])); + $this->assertTrue(isset($definitions[$rootDefinitionKey]['properties'])); + $properties = $resultSchema['definitions'][$rootDefinitionKey]['properties']; + $this->assertArrayHasKey('data', $properties); + $this->assertEquals( + [ + 'type' => 'object', + 'properties' => [ + 'id' => [ + 'type' => 'string', + ], + 'type' => [ + 'type' => 'string', + ], + 'attributes' => [ + '$ref' => '#/definitions/Dummy', + ], + ], + 'required' => [ + 'type', + 'id', + ], + ], + $properties['data'] + ); + } + + public function testSchemaTypeBuildSchema(): void + { + $resultSchema = $this->schemaFactory->buildSchema(Dummy::class, 'jsonapi', Schema::TYPE_OUTPUT, new GetCollection()); + + $this->assertNull($resultSchema->getRootDefinitionKey()); + $this->assertTrue(isset($resultSchema['allOf'][0]['$ref'])); + $this->assertEquals($resultSchema['allOf'][0]['$ref'], '#/definitions/JsonApiCollectionBaseSchema'); + + $jsonApiCollectionBaseSchema = $resultSchema['definitions']['JsonApiCollectionBaseSchema']; + $this->assertTrue(isset($jsonApiCollectionBaseSchema['properties'])); + $this->assertArrayHasKey('links', $jsonApiCollectionBaseSchema['properties']); + $this->assertArrayHasKey('self', $jsonApiCollectionBaseSchema['properties']['links']['properties']); + $this->assertArrayHasKey('first', $jsonApiCollectionBaseSchema['properties']['links']['properties']); + $this->assertArrayHasKey('prev', $jsonApiCollectionBaseSchema['properties']['links']['properties']); + $this->assertArrayHasKey('next', $jsonApiCollectionBaseSchema['properties']['links']['properties']); + $this->assertArrayHasKey('last', $jsonApiCollectionBaseSchema['properties']['links']['properties']); + + $this->assertArrayHasKey('meta', $jsonApiCollectionBaseSchema['properties']); + $this->assertArrayHasKey('totalItems', $jsonApiCollectionBaseSchema['properties']['meta']['properties']); + $this->assertArrayHasKey('itemsPerPage', $jsonApiCollectionBaseSchema['properties']['meta']['properties']); + $this->assertArrayHasKey('currentPage', $jsonApiCollectionBaseSchema['properties']['meta']['properties']); + + $objectSchema = $resultSchema['allOf'][1]; + $this->assertArrayHasKey('data', $objectSchema['properties']); + + $this->assertArrayHasKey('items', $objectSchema['properties']['data']); + $this->assertArrayHasKey('$ref', $objectSchema['properties']['data']['items']['properties']['attributes']); + + $properties = $objectSchema['properties']; + $this->assertArrayHasKey('data', $properties); + $this->assertArrayHasKey('items', $properties['data']); + $this->assertArrayHasKey('id', $properties['data']['items']['properties']); + $this->assertArrayHasKey('type', $properties['data']['items']['properties']); + $this->assertArrayHasKey('attributes', $properties['data']['items']['properties']); + + $forcedCollection = $this->schemaFactory->buildSchema(Dummy::class, 'jsonapi', Schema::TYPE_OUTPUT, forceCollection: true); + $this->assertEquals($resultSchema['allOf'][0]['$ref'], $forcedCollection['allOf'][0]['$ref']); + } +} diff --git a/src/JsonApi/Tests/Serializer/CollectionNormalizerTest.php b/src/JsonApi/Tests/Serializer/CollectionNormalizerTest.php new file mode 100644 index 00000000000..62ca83a51bc --- /dev/null +++ b/src/JsonApi/Tests/Serializer/CollectionNormalizerTest.php @@ -0,0 +1,379 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Tests\Serializer; + +use ApiPlatform\JsonApi\Serializer\CollectionNormalizer; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\State\Pagination\PaginatorInterface; +use ApiPlatform\State\Pagination\PartialPaginatorInterface; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * @author Amrouche Hamza + */ +class CollectionNormalizerTest extends TestCase +{ + use ProphecyTrait; + + public function testSupportsNormalize(): void + { + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); + + $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, ['resource_class' => 'Foo'])); + $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, ['resource_class' => 'Foo', 'api_sub_level' => true])); + $this->assertTrue($normalizer->supportsNormalization([], CollectionNormalizer::FORMAT, [])); + $this->assertTrue($normalizer->supportsNormalization(new \ArrayObject(), CollectionNormalizer::FORMAT, ['resource_class' => 'Foo'])); + $this->assertFalse($normalizer->supportsNormalization([], 'xml', ['resource_class' => 'Foo'])); + $this->assertFalse($normalizer->supportsNormalization(new \ArrayObject(), 'xml', ['resource_class' => 'Foo'])); + $this->assertEmpty($normalizer->getSupportedTypes('json')); + $this->assertSame([ + 'native-array' => true, + '\Traversable' => true, + ], $normalizer->getSupportedTypes($normalizer::FORMAT)); + } + + public function testNormalizePaginator(): void + { + $paginatorProphecy = $this->prophesize(PaginatorInterface::class); + $paginatorProphecy->getCurrentPage()->willReturn(3.); + $paginatorProphecy->getLastPage()->willReturn(7.); + $paginatorProphecy->getItemsPerPage()->willReturn(12.); + $paginatorProphecy->getTotalItems()->willReturn(1312.); + $paginatorProphecy->rewind()->will(function (): void {}); + $paginatorProphecy->next()->will(function (): void {}); + $paginatorProphecy->current()->willReturn('foo'); + $paginatorProphecy->valid()->willReturn(true, false); + + $paginator = $paginatorProphecy->reveal(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($paginator, 'Foo')->willReturn('Foo'); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadataCollection('Foo', [ + (new ApiResource()) + ->withShortName('Foo') + ->withOperations(new Operations(['get' => (new GetCollection())])), + ])); + + $itemNormalizer = $this->prophesize(NormalizerInterface::class); + $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ + 'request_uri' => '/foos?page=3', + 'uri' => 'http://example.com/foos?page=3', + 'api_sub_level' => true, + 'resource_class' => 'Foo', + 'root_operation_name' => 'get', + ])->willReturn([ + 'data' => [ + 'type' => 'Foo', + 'id' => 1, + 'attributes' => [ + 'id' => 1, + 'name' => 'Kévin', + ], + ], + ]); + + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); + $normalizer->setNormalizer($itemNormalizer->reveal()); + + $expected = [ + 'links' => [ + 'self' => '/foos?page=3', + 'first' => '/foos?page=1', + 'last' => '/foos?page=7', + 'prev' => '/foos?page=2', + 'next' => '/foos?page=4', + ], + 'data' => [ + [ + 'type' => 'Foo', + 'id' => 1, + 'attributes' => [ + 'id' => 1, + 'name' => 'Kévin', + ], + ], + ], + 'meta' => [ + 'totalItems' => 1312, + 'itemsPerPage' => 12, + 'currentPage' => 3, + ], + ]; + + $this->assertEquals($expected, $normalizer->normalize($paginator, CollectionNormalizer::FORMAT, [ + 'request_uri' => '/foos?page=3', + 'operation_name' => 'get', + 'uri' => 'http://example.com/foos?page=3', + 'resource_class' => 'Foo', + ])); + } + + public function testNormalizePartialPaginator(): void + { + $paginatorProphecy = $this->prophesize(PartialPaginatorInterface::class); + $paginatorProphecy->getCurrentPage()->willReturn(3.); + $paginatorProphecy->getItemsPerPage()->willReturn(12.); + $paginatorProphecy->rewind()->will(function (): void {}); + $paginatorProphecy->next()->will(function (): void {}); + $paginatorProphecy->current()->willReturn('foo'); + $paginatorProphecy->valid()->willReturn(true, false); + $paginatorProphecy->count()->willReturn(1312); + + $paginator = $paginatorProphecy->reveal(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($paginator, 'Foo')->willReturn('Foo'); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadataCollection('Foo', [ + (new ApiResource()) + ->withShortName('Foo') + ->withOperations(new Operations(['get' => (new GetCollection())])), + ])); + + $itemNormalizer = $this->prophesize(NormalizerInterface::class); + $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ + 'request_uri' => '/foos?page=3', + 'uri' => 'http://example.com/foos?page=3', + 'api_sub_level' => true, + 'resource_class' => 'Foo', + 'root_operation_name' => 'get', + ])->willReturn([ + 'data' => [ + 'type' => 'Foo', + 'id' => 1, + 'attributes' => [ + 'id' => 1, + 'name' => 'Kévin', + ], + ], + ]); + + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); + $normalizer->setNormalizer($itemNormalizer->reveal()); + + $expected = [ + 'links' => [ + 'self' => '/foos?page=3', + 'prev' => '/foos?page=2', + 'next' => '/foos?page=4', + ], + 'data' => [ + [ + 'type' => 'Foo', + 'id' => 1, + 'attributes' => [ + 'id' => 1, + 'name' => 'Kévin', + ], + ], + ], + 'meta' => [ + 'itemsPerPage' => 12, + 'currentPage' => 3, + ], + ]; + + $this->assertEquals($expected, $normalizer->normalize($paginator, CollectionNormalizer::FORMAT, [ + 'request_uri' => '/foos?page=3', + 'operation_name' => 'get', + 'uri' => 'http://example.com/foos?page=3', + 'resource_class' => 'Foo', + ])); + } + + public function testNormalizeArray(): void + { + $data = ['foo']; + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($data, 'Foo')->willReturn('Foo'); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadataCollection('Foo', [ + (new ApiResource()) + ->withShortName('Foo') + ->withOperations(new Operations(['get' => (new GetCollection())])), + ])); + $itemNormalizer = $this->prophesize(NormalizerInterface::class); + $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ + 'request_uri' => '/foos', + 'uri' => 'http://example.com/foos', + 'api_sub_level' => true, + 'resource_class' => 'Foo', + 'root_operation_name' => 'get', + ])->willReturn([ + 'data' => [ + 'type' => 'Foo', + 'id' => 1, + 'attributes' => [ + 'id' => 1, + 'name' => 'Baptiste', + ], + ], + ]); + + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); + $normalizer->setNormalizer($itemNormalizer->reveal()); + + $expected = [ + 'links' => ['self' => '/foos'], + 'data' => [ + [ + 'type' => 'Foo', + 'id' => 1, + 'attributes' => [ + 'id' => 1, + 'name' => 'Baptiste', + ], + ], + ], + 'meta' => ['totalItems' => 1], + ]; + + $this->assertEquals($expected, $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ + 'request_uri' => '/foos', + 'operation_name' => 'get', + 'uri' => 'http://example.com/foos', + 'resource_class' => 'Foo', + ])); + } + + public function testNormalizeIncludedData(): void + { + $data = ['foo']; + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($data, 'Foo')->willReturn('Foo'); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadataCollection('Foo', [ + (new ApiResource()) + ->withShortName('Foo') + ->withOperations(new Operations(['get' => (new GetCollection())])), + ])); + + $itemNormalizer = $this->prophesize(NormalizerInterface::class); + $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ + 'request_uri' => '/foos', + 'uri' => 'http://example.com/foos', + 'api_sub_level' => true, + 'resource_class' => 'Foo', + 'root_operation_name' => 'get', + ])->willReturn([ + 'data' => [ + 'type' => 'Foo', + 'id' => 1, + 'attributes' => [ + 'id' => 1, + 'name' => 'Baptiste', + ], + ], + 'included' => [ + [ + 'type' => 'Bar', + 'id' => 1, + 'attributes' => [ + 'id' => 1, + 'name' => 'Anto', + ], + ], + ], + ]); + + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); + $normalizer->setNormalizer($itemNormalizer->reveal()); + + $expected = [ + 'links' => ['self' => '/foos'], + 'data' => [ + [ + 'type' => 'Foo', + 'id' => 1, + 'attributes' => [ + 'id' => 1, + 'name' => 'Baptiste', + ], + ], + ], + 'meta' => ['totalItems' => 1], + 'included' => [ + [ + 'type' => 'Bar', + 'id' => 1, + 'attributes' => [ + 'id' => 1, + 'name' => 'Anto', + ], + ], + ], + ]; + + $this->assertEquals($expected, $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ + 'request_uri' => '/foos', + 'operation_name' => 'get', + 'uri' => 'http://example.com/foos', + 'resource_class' => 'Foo', + ])); + } + + public function testNormalizeWithoutDataKey(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('The JSON API document must contain a "data" key.'); + + $data = ['foo']; + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($data, 'Foo')->willReturn('Foo'); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn(new ResourceMetadataCollection('Foo', [ + (new ApiResource()) + ->withShortName('Foo') + ->withOperations(new Operations(['get' => (new GetCollection())])), + ])); + + $itemNormalizer = $this->prophesize(NormalizerInterface::class); + $itemNormalizer->normalize('foo', CollectionNormalizer::FORMAT, [ + 'request_uri' => '/foos', + 'uri' => 'http://example.com/foos', + 'api_sub_level' => true, + 'resource_class' => 'Foo', + 'root_operation_name' => 'get', + ])->willReturn([]); + + $normalizer = new CollectionNormalizer($resourceClassResolverProphecy->reveal(), 'page', $resourceMetadataFactoryProphecy->reveal()); + $normalizer->setNormalizer($itemNormalizer->reveal()); + + $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ + 'request_uri' => '/foos', + 'operation_name' => 'get', + 'uri' => 'http://example.com/foos', + 'resource_class' => 'Foo', + ]); + } +} diff --git a/src/JsonApi/Tests/Serializer/ConstraintViolationNormalizerTest.php b/src/JsonApi/Tests/Serializer/ConstraintViolationNormalizerTest.php new file mode 100644 index 00000000000..5b36848cede --- /dev/null +++ b/src/JsonApi/Tests/Serializer/ConstraintViolationNormalizerTest.php @@ -0,0 +1,153 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Tests\Serializer; + +use ApiPlatform\JsonApi\Serializer\ConstraintViolationListNormalizer; +use ApiPlatform\JsonApi\Tests\Fixtures\Dummy; +use ApiPlatform\JsonApi\Tests\Fixtures\RelatedDummy; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\Validator\ConstraintViolation; +use Symfony\Component\Validator\ConstraintViolationList; +use Symfony\Component\Validator\ConstraintViolationListInterface; + +/** + * @author Baptiste Meyer + */ +class ConstraintViolationNormalizerTest extends TestCase +{ + use ProphecyTrait; + + public function testSupportNormalization(): void + { + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $nameConverterInterface = $this->prophesize(NameConverterInterface::class); + + $normalizer = new ConstraintViolationListNormalizer($propertyMetadataFactoryProphecy->reveal(), $nameConverterInterface->reveal()); + + $this->assertTrue($normalizer->supportsNormalization(new ConstraintViolationList(), ConstraintViolationListNormalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization(new ConstraintViolationList(), 'xml')); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), ConstraintViolationListNormalizer::FORMAT)); + $this->assertEmpty($normalizer->getSupportedTypes('json')); + $this->assertSame([ConstraintViolationListInterface::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); + } + + #[IgnoreDeprecations] + public function testNormalize(): void + { + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy')->willReturn((new ApiProperty())->withNativeType(Type::object(RelatedDummy::class)))->shouldBeCalledTimes(1); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name')->willReturn((new ApiProperty())->withNativeType(Type::string()))->shouldBeCalledTimes(1); + + $nameConverterProphecy = $this->prophesize(NameConverterInterface::class); + $nameConverterProphecy->normalize('relatedDummy', Dummy::class, 'jsonapi')->willReturn('relatedDummy')->shouldBeCalledTimes(1); + $nameConverterProphecy->normalize('name', Dummy::class, 'jsonapi')->willReturn('name')->shouldBeCalledTimes(1); + + $dummy = new Dummy(); + + $constraintViolationList = new ConstraintViolationList([ + new ConstraintViolation('This value should not be null.', 'This value should not be null.', [], $dummy, 'relatedDummy', null), + new ConstraintViolation('This value should not be null.', 'This value should not be null.', [], $dummy, 'name', null), + new ConstraintViolation('Unknown violation.', 'Unknown violation.', [], $dummy, '', ''), + ]); + + $this->assertEquals( + [ + 'errors' => [ + [ + 'detail' => 'This value should not be null.', + 'source' => [ + 'pointer' => 'data/relationships/relatedDummy', + ], + ], + [ + 'detail' => 'This value should not be null.', + 'source' => [ + 'pointer' => 'data/attributes/name', + ], + ], + [ + 'detail' => 'Unknown violation.', + 'source' => [ + 'pointer' => 'data', + ], + ], + ], + ], + (new ConstraintViolationListNormalizer($propertyMetadataFactoryProphecy->reveal(), $nameConverterProphecy->reveal()))->normalize($constraintViolationList) + ); + } + + public function testNormalizeWithStringRoot(): void + { + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + + // Create a violation with a string root (simulating query parameter validation) + $constraintViolationList = new ConstraintViolationList([ + new ConstraintViolation('Invalid page value.', 'Invalid page value.', [], 'page', 'page', 'invalid'), + ]); + + $normalizer = new ConstraintViolationListNormalizer($propertyMetadataFactoryProphecy->reveal()); + + $result = $normalizer->normalize($constraintViolationList); + + $this->assertEquals( + [ + 'errors' => [ + [ + 'detail' => 'Invalid page value.', + 'source' => [ + 'pointer' => 'data/attributes/page', + ], + ], + ], + ], + $result + ); + } + + public function testNormalizeWithNullRoot(): void + { + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + + // Create a violation with a null root + $constraintViolationList = new ConstraintViolationList([ + new ConstraintViolation('Invalid value.', 'Invalid value.', [], null, 'field', 'invalid'), + ]); + + $normalizer = new ConstraintViolationListNormalizer($propertyMetadataFactoryProphecy->reveal()); + + // This should not throw a TypeError and should handle the null root gracefully + $result = $normalizer->normalize($constraintViolationList); + + $this->assertEquals( + [ + 'errors' => [ + [ + 'detail' => 'Invalid value.', + 'source' => [ + 'pointer' => 'data/attributes/field', + ], + ], + ], + ], + $result + ); + } +} diff --git a/src/JsonApi/Tests/Serializer/EntrypointNormalizerTest.php b/src/JsonApi/Tests/Serializer/EntrypointNormalizerTest.php new file mode 100644 index 00000000000..23432530ed7 --- /dev/null +++ b/src/JsonApi/Tests/Serializer/EntrypointNormalizerTest.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Tests\Serializer; + +use ApiPlatform\Documentation\Entrypoint; +use ApiPlatform\JsonApi\Serializer\EntrypointNormalizer; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyCar; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; + +/** + * @author Amrouche Hamza + */ +class EntrypointNormalizerTest extends TestCase +{ + use ProphecyTrait; + + public function testSupportNormalization(): void + { + $collection = new ResourceNameCollection(); + $entrypoint = new Entrypoint($collection); + + $factoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + + $normalizer = new EntrypointNormalizer($factoryProphecy->reveal(), $iriConverterProphecy->reveal(), $urlGeneratorProphecy->reveal()); + + $this->assertTrue($normalizer->supportsNormalization($entrypoint, EntrypointNormalizer::FORMAT)); + $this->assertFalse($normalizer->supportsNormalization($entrypoint, 'json')); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass(), EntrypointNormalizer::FORMAT)); + $this->assertEmpty($normalizer->getSupportedTypes('json')); + $this->assertSame([Entrypoint::class => true], $normalizer->getSupportedTypes($normalizer::FORMAT)); + } + + public function testNormalize(): void + { + $collection = new ResourceNameCollection([Dummy::class, RelatedDummy::class, DummyCar::class]); + $entrypoint = new Entrypoint($collection); + $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [ + (new ApiResource())->withShortName('Dummy')->withOperations(new Operations([ + 'get' => (new GetCollection())->withShortName('Dummy'), + ])), + ]))->shouldBeCalled(); + $resourceMetadataCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn(new ResourceMetadataCollection('RelatedDummy', [ + (new ApiResource())->withShortName('RelatedDummy')->withOperations(new Operations([ + 'get' => (new Get())->withShortName('RelatedDummy'), + ])), + ]))->shouldBeCalled(); + $resourceMetadataCollectionFactoryProphecy->create(DummyCar::class)->willReturn(new ResourceMetadataCollection('DummyCar', [ + (new ApiResource())->withShortName('DummyCar')->withOperations(new Operations([ + 'post' => (new Post())->withShortName('DummyCar'), + ])), + ]))->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_URL, Argument::type(Operation::class))->willReturn('http://localhost/api/dummies')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource(RelatedDummy::class, UrlGeneratorInterface::ABS_URL, Argument::type(Operation::class))->shouldNotBeCalled(); + $iriConverterProphecy->getIriFromResource(DummyCar::class, UrlGeneratorInterface::ABS_URL, Argument::type(Operation::class))->shouldNotBeCalled(); + + $urlGeneratorProphecy = $this->prophesize(UrlGeneratorInterface::class); + $urlGeneratorProphecy->generate('api_entrypoint', [], UrlGeneratorInterface::ABS_URL)->willReturn('http://localhost/api')->shouldBeCalled(); + + $this->assertEquals( + [ + 'links' => [ + 'self' => 'http://localhost/api', + 'dummy' => 'http://localhost/api/dummies', + ], + ], + (new EntrypointNormalizer($resourceMetadataCollectionFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $urlGeneratorProphecy->reveal()))->normalize($entrypoint, EntrypointNormalizer::FORMAT) + ); + } +} diff --git a/src/JsonApi/Tests/Serializer/ItemNormalizerTest.php b/src/JsonApi/Tests/Serializer/ItemNormalizerTest.php new file mode 100644 index 00000000000..d6bea46d1ca --- /dev/null +++ b/src/JsonApi/Tests/Serializer/ItemNormalizerTest.php @@ -0,0 +1,491 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonApi\Tests\Serializer; + +use ApiPlatform\JsonApi\Serializer\ItemNormalizer; +use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter; +use ApiPlatform\JsonApi\Tests\Fixtures\CircularReference; +use ApiPlatform\JsonApi\Tests\Fixtures\Dummy; +use ApiPlatform\JsonApi\Tests\Fixtures\RelatedDummy; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Doctrine\Common\Collections\ArrayCollection; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\HttpFoundation\EventStreamResponse; +use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\TypeInfo\Type; + +/** + * @author Amrouche Hamza + */ +class ItemNormalizerTest extends TestCase +{ + use ProphecyTrait; + + public function testNormalize(): void + { + $dummy = new Dummy(); + $dummy->setId(10); + $dummy->setName('hello'); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->willReturn(new PropertyNameCollection(['id', 'name', '\bad_property'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->willReturn((new ApiProperty())->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', Argument::type('array'))->willReturn((new ApiProperty())->withReadable(true)->withIdentifier(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, '\bad_property', Argument::any())->willReturn((new ApiProperty())->withReadable(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/10'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass($dummy, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->getValue($dummy, 'id')->willReturn(10); + $propertyAccessorProphecy->getValue($dummy, 'name')->willReturn('hello'); + + $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [ + (new ApiResource()) + ->withShortName('Dummy') + ->withDescription('A dummy') + ->withUriTemplate('/dummy') + ->withOperations(new Operations(['get' => (new Get())->withShortName('Dummy')])), + ])); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $serializerProphecy->normalize('hello', ItemNormalizer::FORMAT, Argument::type('array'))->willReturn('hello'); + $serializerProphecy->normalize(10, ItemNormalizer::FORMAT, Argument::type('array'))->willReturn(10); + $serializerProphecy->normalize(null, ItemNormalizer::FORMAT, Argument::type('array'))->willReturn(null); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + new ReservedAttributeNameConverter(), + null, + [], + $resourceMetadataCollectionFactoryProphecy->reveal(), + ); + + $normalizer->setSerializer($serializerProphecy->reveal()); + + $expected = [ + 'data' => [ + 'type' => 'Dummy', + 'id' => '/dummies/10', + 'attributes' => [ + '_id' => 10, + 'name' => 'hello', + ], + ], + ]; + + $this->assertEquals($expected, $normalizer->normalize($dummy, ItemNormalizer::FORMAT)); + } + + public function testNormalizeCircularReference(): void + { + $circularReferenceEntity = new CircularReference(); + $circularReferenceEntity->id = 1; + $circularReferenceEntity->parent = $circularReferenceEntity; + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($circularReferenceEntity, Argument::cetera())->willReturn('/circular_references/1'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(CircularReference::class)->willReturn(true); + $resourceClassResolverProphecy->getResourceClass($circularReferenceEntity, null)->willReturn(CircularReference::class); + $resourceClassResolverProphecy->getResourceClass(null, CircularReference::class)->willReturn(CircularReference::class); + + $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactoryProphecy->create(CircularReference::class)->willReturn(new ResourceMetadataCollection('CircularReference')); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(CircularReference::class, [])->willReturn(new PropertyNameCollection()); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $this->prophesize(PropertyMetadataFactoryInterface::class)->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $this->prophesize(PropertyAccessorInterface::class)->reveal(), + new ReservedAttributeNameConverter(), + null, + [], + $resourceMetadataCollectionFactoryProphecy->reveal(), + ); + + $normalizer->setSerializer($this->prophesize(SerializerInterface::class)->reveal()); + + // Symfony >= 7.3 + $splObject = class_exists(EventStreamResponse::class) ? spl_object_id($circularReferenceEntity) : spl_object_hash($circularReferenceEntity); + $context = [ + 'circular_reference_limit' => 2, + 'circular_reference_limit_counters' => [$splObject => 2], + 'cache_error' => function (): void {}, + ]; + + $this->assertSame('/circular_references/1', $normalizer->normalize($circularReferenceEntity, ItemNormalizer::FORMAT, $context)); + } + + public function testNormalizeNonExistentProperty(): void + { + $this->expectException(NoSuchPropertyException::class); + + $dummy = new Dummy(); + $dummy->setId(1); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->willReturn(new PropertyNameCollection(['bar'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'bar', Argument::any())->willReturn((new ApiProperty())->withReadable(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass($dummy, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->getValue($dummy, 'bar')->willThrow(new NoSuchPropertyException()); + + $serializerProphecy = $this->prophesize(Serializer::class); + + $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [ + (new ApiResource()) + ->withShortName('Dummy') + ->withOperations(new Operations(['get' => (new Get())->withShortName('Dummy')])), + ])); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + new ReservedAttributeNameConverter(), + null, + [], + $resourceMetadataCollectionFactoryProphecy->reveal(), + ); + + $normalizer->setSerializer($serializerProphecy->reveal()); + + $normalizer->normalize($dummy, ItemNormalizer::FORMAT); + } + + public function testDenormalize(): void + { + $data = [ + 'data' => [ + 'type' => 'dummy', + 'attributes' => [ + 'name' => 'foo', + 'ghost' => 'invisible', + ], + 'relationships' => [ + 'relatedDummy' => [ + 'data' => [ + 'type' => 'related-dummy', + 'id' => '/related_dummies/1', + ], + ], + 'relatedDummies' => [ + 'data' => [ + [ + 'type' => 'related-dummy', + 'id' => '/related_dummies/2', + ], + ], + ], + ], + ], + ]; + + $relatedDummy1 = new RelatedDummy(); + $relatedDummy1->setId(1); + $relatedDummy2 = new RelatedDummy(); + $relatedDummy2->setId(2); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::any())->willReturn(new PropertyNameCollection(['name', 'ghost', 'relatedDummy', 'relatedDummies'])); + + $relatedDummyType = Type::object(RelatedDummy::class); + $relatedDummiesType = Type::collection(Type::object(ArrayCollection::class), Type::object(RelatedDummy::class), Type::int()); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::any())->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'ghost', Argument::any())->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', Argument::any())->willReturn((new ApiProperty())->withNativeType($relatedDummyType)->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', Argument::any())->willReturn((new ApiProperty())->withNativeType($relatedDummiesType)->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + $getItemFromIriSecondArgCallback = fn ($arg): bool => \is_array($arg) && isset($arg['fetch_data']) && true === $arg['fetch_data']; + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getResourceFromIri('/related_dummies/1', Argument::that($getItemFromIriSecondArgCallback))->willReturn($relatedDummy1); + $iriConverterProphecy->getResourceFromIri('/related_dummies/2', Argument::that($getItemFromIriSecondArgCallback))->willReturn($relatedDummy2); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->setValue(Argument::type(Dummy::class), 'name', 'foo')->will(function (): void {}); + $propertyAccessorProphecy->setValue(Argument::type(Dummy::class), 'ghost', 'invisible')->willThrow(new NoSuchPropertyException()); + $propertyAccessorProphecy->setValue(Argument::type(Dummy::class), 'relatedDummy', $relatedDummy1)->will(function (): void {}); + $propertyAccessorProphecy->setValue(Argument::type(Dummy::class), 'relatedDummies', [$relatedDummy2])->will(function (): void {}); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + + $resourceMetadataCollectionFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactory->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ + (new ApiResource())->withOperations(new Operations([new Get(name: 'get')])), + ] + )); + $resourceMetadataCollectionFactory->create(RelatedDummy::class)->willReturn(new ResourceMetadataCollection(RelatedDummy::class, [ + (new ApiResource())->withOperations(new Operations([new Get(name: 'get')])), + ] + )); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + new ReservedAttributeNameConverter(), + null, + [], + $resourceMetadataCollectionFactory->reveal(), + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertInstanceOf(Dummy::class, $normalizer->denormalize($data, Dummy::class, ItemNormalizer::FORMAT)); + } + + public function testDenormalizeUpdateOperationNotAllowed(): void + { + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('Update is not allowed for this operation.'); + + $normalizer = new ItemNormalizer( + $this->prophesize(PropertyNameCollectionFactoryInterface::class)->reveal(), + $this->prophesize(PropertyMetadataFactoryInterface::class)->reveal(), + $this->prophesize(IriConverterInterface::class)->reveal(), + $this->prophesize(ResourceClassResolverInterface::class)->reveal(), + ); + + $normalizer->denormalize( + [ + 'data' => [ + 'id' => 1, + 'type' => 'dummy', + ], + ], + Dummy::class, + ItemNormalizer::FORMAT, + [ + 'api_allow_update' => false, + ] + ); + } + + public function testDenormalizeCollectionIsNotArray(): void + { + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('The type of the "relatedDummies" attribute must be "array", "string" given.'); + + $data = [ + 'data' => [ + 'type' => 'dummy', + 'relationships' => [ + 'relatedDummies' => [ + 'data' => 'foo', + ], + ], + ], + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['relatedDummies'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + + $type = Type::collection(Type::object(ArrayCollection::class), Type::object(RelatedDummy::class), Type::int()); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', Argument::type('array'))->willReturn((new ApiProperty()) + ->withNativeType($type) + ->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + new ReservedAttributeNameConverter(), + null, + [] + ); + + $normalizer->denormalize($data, Dummy::class, ItemNormalizer::FORMAT); + } + + public function testDenormalizeCollectionWithInvalidKey(): void + { + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('The type of the key "0" must be "string", "integer" given.'); + + $data = [ + 'data' => [ + 'type' => 'dummy', + 'relationships' => [ + 'relatedDummies' => [ + 'data' => [ + [ + 'type' => 'related-dummy', + 'id' => '2', + ], + ], + ], + ], + ], + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['relatedDummies'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $type = Type::collection(Type::object(ArrayCollection::class), Type::object(RelatedDummy::class), Type::string()); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType($type)->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + new ReservedAttributeNameConverter(), + null, + [] + ); + + $normalizer->denormalize($data, Dummy::class, ItemNormalizer::FORMAT); + } + + public function testDenormalizeRelationIsNotResourceLinkage(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Only resource linkage supported currently, see: http://jsonapi.org/format/#document-resource-object-linkage.'); + + $data = [ + 'data' => [ + 'type' => 'dummy', + 'relationships' => [ + 'relatedDummy' => [ + 'data' => 'foo', + ], + ], + ], + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['relatedDummy'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', Argument::type('array'))->willReturn( + (new ApiProperty())->withNativeType(Type::object(RelatedDummy::class))->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + new ReservedAttributeNameConverter(), + null, + [] + ); + + $normalizer->denormalize($data, Dummy::class, ItemNormalizer::FORMAT); + } +} diff --git a/tests/JsonApi/Serializer/ReservedAttributeNameConverterTest.php b/src/JsonApi/Tests/Serializer/ReservedAttributeNameConverterTest.php similarity index 86% rename from tests/JsonApi/Serializer/ReservedAttributeNameConverterTest.php rename to src/JsonApi/Tests/Serializer/ReservedAttributeNameConverterTest.php index fbcd0368a94..7c261af1cc7 100644 --- a/tests/JsonApi/Serializer/ReservedAttributeNameConverterTest.php +++ b/src/JsonApi/Tests/Serializer/ReservedAttributeNameConverterTest.php @@ -11,10 +11,10 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\JsonApi\Serializer; +namespace ApiPlatform\JsonApi\Tests\Serializer; use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter; -use ApiPlatform\Tests\Fixtures\TestBundle\Serializer\NameConverter\CustomConverter; +use ApiPlatform\JsonApi\Tests\Fixtures\CustomConverter; use PHPUnit\Framework\TestCase; /** @@ -46,17 +46,13 @@ public static function propertiesProvider(): array ]; } - /** - * @dataProvider propertiesProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('propertiesProvider')] public function testNormalize(string $propertyName, string $expectedPropertyName): void { $this->assertSame($expectedPropertyName, $this->reservedAttributeNameConverter->normalize($propertyName)); } - /** - * @dataProvider propertiesProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('propertiesProvider')] public function testDenormalize(string $expectedPropertyName, string $propertyName): void { $this->assertSame($expectedPropertyName, $this->reservedAttributeNameConverter->denormalize($propertyName)); diff --git a/tests/JsonApi/State/JsonApiProviderTest.php b/src/JsonApi/Tests/State/JsonApiProviderTest.php similarity index 79% rename from tests/JsonApi/State/JsonApiProviderTest.php rename to src/JsonApi/Tests/State/JsonApiProviderTest.php index 30e9106e584..5ce94261026 100644 --- a/tests/JsonApi/State/JsonApiProviderTest.php +++ b/src/JsonApi/Tests/State/JsonApiProviderTest.php @@ -11,12 +11,13 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\JsonApi\State; +namespace ApiPlatform\JsonApi\Tests\State; use ApiPlatform\JsonApi\State\JsonApiProvider; use ApiPlatform\Metadata\Get; use ApiPlatform\State\ProviderInterface; use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\InputBag; use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; @@ -29,9 +30,8 @@ public function testProvide(): void $request->attributes = $this->createMock(ParameterBag::class); $request->attributes->expects($this->once())->method('get')->with('_api_filters', [])->willReturn([]); $request->attributes->method('set')->with($this->logicalOr('_api_filter_property', '_api_included', '_api_filters'), $this->logicalOr(['id', 'name', 'dummyFloat', 'relatedDummy' => ['id', 'name']], ['relatedDummy'], [])); - $request->query = $this->createMock(ParameterBag::class); // @phpstan-ignore-line - $request->query->method('all')->willReturn(['fields' => ['dummy' => 'id,name,dummyFloat', 'relatedDummy' => 'id,name'], 'include' => 'relatedDummy,foo']); - $operation = new Get(class: 'dummy', shortName: 'dummy'); + $request->query = new InputBag(['fields' => ['dummy' => 'id,name,dummyFloat', 'relatedDummy' => 'id,name'], 'include' => 'relatedDummy,foo']); + $operation = new Get(class: \stdClass::class, shortName: 'dummy'); $context = ['request' => $request]; $decorated = $this->createMock(ProviderInterface::class); $provider = new JsonApiProvider($decorated); diff --git a/src/JsonApi/composer.json b/src/JsonApi/composer.json new file mode 100644 index 00000000000..d02efacf500 --- /dev/null +++ b/src/JsonApi/composer.json @@ -0,0 +1,82 @@ +{ + "name": "api-platform/json-api", + "description": "API JSON-API support", + "type": "library", + "keywords": [ + "REST", + "API", + "JSONAPI" + ], + "homepage": "https://api-platform.com/", + "license": "MIT", + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr/" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "require": { + "php": ">=8.2", + "api-platform/documentation": "^4.1.11", + "api-platform/json-schema": "^4.2@beta", + "api-platform/metadata": "^4.2@beta", + "api-platform/serializer": "^4.1.11", + "api-platform/state": "^4.1.11", + "symfony/error-handler": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/type-info": "^7.3" + }, + "require-dev": { + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "11.5.x-dev", + "symfony/type-info": "^7.3" + }, + "autoload": { + "psr-4": { + "ApiPlatform\\JsonApi\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "phpstan/extension-installer": true + } + }, + "extra": { + "branch-alias": { + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" + }, + "symfony": { + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" + } + }, + "scripts": { + "test": "./vendor/bin/phpunit" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] +} diff --git a/src/JsonApi/phpunit.baseline.xml b/src/JsonApi/phpunit.baseline.xml new file mode 100644 index 00000000000..e3ef6196399 --- /dev/null +++ b/src/JsonApi/phpunit.baseline.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/JsonApi/phpunit.xml.dist b/src/JsonApi/phpunit.xml.dist new file mode 100644 index 00000000000..27bf85ce594 --- /dev/null +++ b/src/JsonApi/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + + + + ./Tests/ + + + + + + trigger_deprecation + + + ./ + + + ./Tests + ./vendor + + + diff --git a/src/JsonLd/.gitattributes b/src/JsonLd/.gitattributes index ae3c2e1685a..801f2080d71 100644 --- a/src/JsonLd/.gitattributes +++ b/src/JsonLd/.gitattributes @@ -1,2 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore /.gitignore export-ignore /Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/JsonLd/.github/workflows/close_pr.yml b/src/JsonLd/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/JsonLd/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/JsonLd/Action/ContextAction.php b/src/JsonLd/Action/ContextAction.php index c53f5d08774..74c144ed81b 100644 --- a/src/JsonLd/Action/ContextAction.php +++ b/src/JsonLd/Action/ContextAction.php @@ -56,6 +56,10 @@ public function __construct( */ public function __invoke(string $shortName = 'Entrypoint', ?Request $request = null): array|Response { + if (!$shortName) { + $shortName = 'Entrypoint'; + } + if (null !== $request && $this->provider && $this->processor && $this->serializer) { $operation = new Get( outputFormats: ['jsonld' => ['application/ld+json']], diff --git a/src/JsonLd/ContextBuilder.php b/src/JsonLd/ContextBuilder.php index c3eb26e70b6..65bb2a68e37 100644 --- a/src/JsonLd/ContextBuilder.php +++ b/src/JsonLd/ContextBuilder.php @@ -13,6 +13,7 @@ namespace ApiPlatform\JsonLd; +use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\HttpOperation; use ApiPlatform\Metadata\IriConverterInterface; @@ -32,10 +33,13 @@ final class ContextBuilder implements AnonymousContextBuilderInterface { use ClassInfoTrait; + use HydraPrefixTrait; public const FORMAT = 'jsonld'; + public const HYDRA_PREFIX = 'hydra:'; + public const HYDRA_CONTEXT_HAS_PREFIX = 'hydra_prefix'; - public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly UrlGeneratorInterface $urlGenerator, private readonly ?IriConverterInterface $iriConverter = null, private readonly ?NameConverterInterface $nameConverter = null) + public function __construct(private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly UrlGeneratorInterface $urlGenerator, private readonly ?IriConverterInterface $iriConverter = null, private readonly ?NameConverterInterface $nameConverter = null, private array $defaultContext = []) { } @@ -81,9 +85,10 @@ public function getResourceContext(string $resourceClass, int $referenceType = U return []; } - if ($operation->getNormalizationContext()['iri_only'] ?? false) { + $context = $operation->getNormalizationContext(); + if ($context['iri_only'] ?? false) { $context = $this->getBaseContext($referenceType); - $context['hydra:member']['@type'] = '@id'; + $context[$this->getHydraPrefix($context).'member']['@type'] = '@id'; return $context; } @@ -110,14 +115,24 @@ public function getResourceContextUri(string $resourceClass, ?int $referenceType public function getAnonymousResourceContext(object $object, array $context = [], int $referenceType = UrlGeneratorInterface::ABS_PATH): array { $outputClass = $this->getObjectClass($object); - $operation = $context['operation'] ?? new Get(shortName: (new \ReflectionClass($outputClass))->getShortName()); + $operation = $context['operation'] ?? new Get( + shortName: (new \ReflectionClass($outputClass))->getShortName(), + normalizationContext: [ + self::HYDRA_CONTEXT_HAS_PREFIX => $context[self::HYDRA_CONTEXT_HAS_PREFIX] ?? $this->defaultContext[self::HYDRA_CONTEXT_HAS_PREFIX] ?? true, + 'groups' => [], + ], + denormalizationContext: [ + 'groups' => [], + ] + ); $shortName = $operation->getShortName(); $jsonLdContext = [ '@context' => $this->getResourceContextWithShortname( $outputClass, $referenceType, - $shortName + $shortName, + $operation ), '@type' => $shortName, ]; @@ -133,6 +148,7 @@ public function getAnonymousResourceContext(object $object, array $context = [], } // here the object can be different from the resource given by the $context['api_resource'] value + // TODO: this is probably not used anymore and is slow we get that @type way earlier, remove this if (isset($context['api_resource'])) { $jsonLdContext['@type'] = $this->resourceMetadataFactory->create($this->getObjectClass($context['api_resource']))[0]->getShortName(); } diff --git a/src/JsonLd/ContextBuilderInterface.php b/src/JsonLd/ContextBuilderInterface.php index 63c34d66c42..c90152c1f83 100644 --- a/src/JsonLd/ContextBuilderInterface.php +++ b/src/JsonLd/ContextBuilderInterface.php @@ -23,6 +23,7 @@ */ interface ContextBuilderInterface { + public const HYDRA_CONTEXT = 'http://www.w3.org/ns/hydra/context.jsonld'; public const HYDRA_NS = 'http://www.w3.org/ns/hydra/core#'; public const JSONLD_NS = 'http://www.w3.org/ns/json-ld#'; public const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; diff --git a/src/JsonLd/HydraContext.php b/src/JsonLd/HydraContext.php new file mode 100644 index 00000000000..7d694b81e51 --- /dev/null +++ b/src/JsonLd/HydraContext.php @@ -0,0 +1,925 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonLd; + +/* + * This is an autogenerated file, DO NOT MODIFY IT. + * Run the update-hydra-context.php script at the root of the project to refresh it. + */ +const HYDRA_CONTEXT = [ + '@context' => [ + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xsd' => 'http://www.w3.org/2001/XMLSchema#', + 'owl' => 'http://www.w3.org/2002/07/owl#', + 'vs' => 'http://www.w3.org/2003/06/sw-vocab-status/ns#', + 'dc' => 'http://purl.org/dc/terms/', + 'cc' => 'http://creativecommons.org/ns#', + 'schema' => 'http://schema.org/', + 'apiDocumentation' => 'hydra:apiDocumentation', + 'ApiDocumentation' => 'hydra:ApiDocumentation', + 'title' => 'hydra:title', + 'description' => 'hydra:description', + 'entrypoint' => [ + '@id' => 'hydra:entrypoint', + '@type' => '@id', + ], + 'supportedClass' => [ + '@id' => 'hydra:supportedClass', + '@type' => '@vocab', + ], + 'Class' => 'hydra:Class', + 'supportedProperty' => [ + '@id' => 'hydra:supportedProperty', + '@type' => '@id', + ], + 'SupportedProperty' => 'hydra:SupportedProperty', + 'property' => [ + '@id' => 'hydra:property', + '@type' => '@vocab', + ], + 'required' => 'hydra:required', + 'readable' => 'hydra:readable', + 'writable' => 'hydra:writable', + 'writeable' => 'hydra:writeable', + 'supportedOperation' => [ + '@id' => 'hydra:supportedOperation', + '@type' => '@id', + ], + 'Operation' => 'hydra:Operation', + 'method' => 'hydra:method', + 'expects' => [ + '@id' => 'hydra:expects', + '@type' => '@vocab', + ], + 'returns' => [ + '@id' => 'hydra:returns', + '@type' => '@vocab', + ], + 'possibleStatus' => [ + '@id' => 'hydra:possibleStatus', + '@type' => '@id', + ], + 'Status' => 'hydra:Status', + 'statusCode' => 'hydra:statusCode', + 'Error' => 'hydra:Error', + 'Resource' => 'hydra:Resource', + 'operation' => 'hydra:operation', + 'Collection' => 'hydra:Collection', + 'collection' => 'hydra:collection', + 'member' => [ + '@id' => 'hydra:member', + '@type' => '@id', + ], + 'memberAssertion' => 'hydra:memberAssertion', + 'manages' => 'hydra:manages', + 'subject' => [ + '@id' => 'hydra:subject', + '@type' => '@vocab', + ], + 'object' => [ + '@id' => 'hydra:object', + '@type' => '@vocab', + ], + 'search' => 'hydra:search', + 'freetextQuery' => 'hydra:freetextQuery', + 'view' => [ + '@id' => 'hydra:view', + '@type' => '@id', + ], + 'PartialCollectionView' => 'hydra:PartialCollectionView', + 'totalItems' => 'hydra:totalItems', + 'first' => [ + '@id' => 'hydra:first', + '@type' => '@id', + ], + 'last' => [ + '@id' => 'hydra:last', + '@type' => '@id', + ], + 'next' => [ + '@id' => 'hydra:next', + '@type' => '@id', + ], + 'previous' => [ + '@id' => 'hydra:previous', + '@type' => '@id', + ], + 'Link' => 'hydra:Link', + 'TemplatedLink' => 'hydra:TemplatedLink', + 'IriTemplate' => 'hydra:IriTemplate', + 'template' => 'hydra:template', + 'Rfc6570Template' => 'hydra:Rfc6570Template', + 'variableRepresentation' => [ + '@id' => 'hydra:variableRepresentation', + '@type' => '@vocab', + ], + 'VariableRepresentation' => 'hydra:VariableRepresentation', + 'BasicRepresentation' => 'hydra:BasicRepresentation', + 'ExplicitRepresentation' => 'hydra:ExplicitRepresentation', + 'mapping' => 'hydra:mapping', + 'IriTemplateMapping' => 'hydra:IriTemplateMapping', + 'variable' => 'hydra:variable', + 'offset' => [ + '@id' => 'hydra:offset', + '@type' => 'xsd:nonNegativeInteger', + ], + 'limit' => [ + '@id' => 'hydra:limit', + '@type' => 'xsd:nonNegativeInteger', + ], + 'pageIndex' => [ + '@id' => 'hydra:pageIndex', + '@type' => 'xsd:nonNegativeInteger', + ], + 'pageReference' => [ + '@id' => 'hydra:pageReference', + ], + 'returnsHeader' => [ + '@id' => 'hydra:returnsHeader', + '@type' => 'xsd:string', + ], + 'expectsHeader' => [ + '@id' => 'hydra:expectsHeader', + '@type' => 'xsd:string', + ], + 'HeaderSpecification' => 'hydra:HeaderSpecification', + 'headerName' => 'hydra:headerName', + 'possibleValue' => 'hydra:possibleValue', + 'closedSet' => [ + '@id' => 'hydra:possibleValue', + '@type' => 'xsd:boolean', + ], + 'name' => [ + '@id' => 'hydra:name', + '@type' => 'xsd:string', + ], + 'extension' => [ + '@id' => 'hydra:extension', + '@type' => '@id', + ], + 'isDefinedBy' => [ + '@id' => 'rdfs:isDefinedBy', + '@type' => '@id', + ], + 'defines' => [ + '@reverse' => 'rdfs:isDefinedBy', + ], + 'comment' => 'rdfs:comment', + 'label' => 'rdfs:label', + 'preferredPrefix' => 'http://purl.org/vocab/vann/preferredNamespacePrefix', + 'cc:license' => [ + '@type' => '@id', + ], + 'cc:attributionURL' => [ + '@type' => '@id', + ], + 'domain' => [ + '@id' => 'rdfs:domain', + '@type' => '@vocab', + ], + 'range' => [ + '@id' => 'rdfs:range', + '@type' => '@vocab', + ], + 'subClassOf' => [ + '@id' => 'rdfs:subClassOf', + '@type' => '@vocab', + ], + 'subPropertyOf' => [ + '@id' => 'rdfs:subPropertyOf', + '@type' => '@vocab', + ], + 'seeAlso' => [ + '@id' => 'rdfs:seeAlso', + '@type' => '@id', + ], + 'domainIncludes' => [ + '@id' => 'schema:domainIncludes', + '@type' => '@id', + ], + 'rangeIncludes' => [ + '@id' => 'schema:rangeIncludes', + '@type' => '@id', + ], + ], + '@id' => 'http://www.w3.org/ns/hydra/core', + '@type' => 'owl:Ontology', + 'label' => 'The Hydra Core Vocabulary', + 'comment' => 'A lightweight vocabulary for hypermedia-driven Web APIs', + 'seeAlso' => 'https://www.hydra-cg.com/spec/latest/core/', + 'preferredPrefix' => 'hydra', + 'dc:description' => 'The Hydra Core Vocabulary is a lightweight vocabulary to create hypermedia-driven Web APIs. By specifying a number of concepts commonly used in Web APIs it enables the creation of generic API clients.', + 'dc:rights' => 'Copyright © 2012-2014 the Contributors to the Hydra Core Vocabulary Specification', + 'dc:publisher' => 'Hydra W3C Community Group', + 'cc:license' => 'http://creativecommons.org/licenses/by/4.0/', + 'cc:attributionName' => 'Hydra W3C Community Group', + 'cc:attributionURL' => 'http://www.hydra-cg.com/', + 'defines' => [ + 0 => [ + '@id' => 'hydra:Resource', + '@type' => 'hydra:Class', + 'label' => 'Hydra Resource', + 'comment' => 'The class of dereferenceable resources by means a client can attempt to dereference; however, the received responses should still be verified.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 1 => [ + '@id' => 'hydra:Class', + '@type' => [ + 0 => 'hydra:Resource', + 1 => 'rdfs:Class', + ], + 'subClassOf' => [ + 0 => 'rdfs:Class', + ], + 'label' => 'Hydra Class', + 'comment' => 'The class of Hydra classes.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 2 => [ + '@id' => 'hydra:Link', + '@type' => 'hydra:Class', + 'subClassOf' => [ + 0 => 'hydra:Resource', + 1 => 'rdf:Property', + ], + 'label' => 'Link', + 'comment' => 'The class of properties representing links.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 3 => [ + '@id' => 'hydra:apiDocumentation', + '@type' => 'hydra:Link', + 'label' => 'apiDocumentation', + 'comment' => 'A link to the API documentation', + 'range' => 'hydra:ApiDocumentation', + 'domain' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 4 => [ + '@id' => 'hydra:ApiDocumentation', + '@type' => 'hydra:Class', + 'subClassOf' => 'hydra:Resource', + 'label' => 'ApiDocumentation', + 'comment' => 'The Hydra API documentation class', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 5 => [ + '@id' => 'hydra:entrypoint', + '@type' => 'hydra:Link', + 'label' => 'entrypoint', + 'comment' => 'A link to main entry point of the Web API', + 'domain' => 'hydra:ApiDocumentation', + 'range' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 6 => [ + '@id' => 'hydra:supportedClass', + '@type' => 'hydra:Link', + 'label' => 'supported classes', + 'comment' => 'A class known to be supported by the Web API', + 'domain' => 'hydra:ApiDocumentation', + 'range' => 'rdfs:Class', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 7 => [ + '@id' => 'hydra:possibleStatus', + '@type' => 'hydra:Link', + 'label' => 'possible status', + 'comment' => 'A status that might be returned by the Web API (other statuses should be expected and properly handled as well)', + 'range' => 'hydra:Status', + 'domainIncludes' => [ + 0 => 'hydra:ApiDocumentation', + 1 => 'hydra:Operation', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 8 => [ + '@id' => 'hydra:supportedProperty', + '@type' => 'hydra:Link', + 'label' => 'supported properties', + 'comment' => 'The properties known to be supported by a Hydra class', + 'domain' => 'rdfs:Class', + 'range' => 'hydra:SupportedProperty', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 9 => [ + '@id' => 'hydra:SupportedProperty', + '@type' => 'hydra:Class', + 'label' => 'Supported Property', + 'comment' => 'A property known to be supported by a Hydra class.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 10 => [ + '@id' => 'hydra:property', + '@type' => 'rdf:Property', + 'label' => 'property', + 'comment' => 'A property', + 'range' => 'rdf:Property', + 'domainIncludes' => [ + 0 => 'hydra:SupportedProperty', + 1 => 'hydra:IriTemplateMapping', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 11 => [ + '@id' => 'hydra:required', + '@type' => 'rdf:Property', + 'label' => 'required', + 'comment' => 'True if the property is required, false otherwise.', + 'range' => 'xsd:boolean', + 'domainIncludes' => [ + 0 => 'hydra:SupportedProperty', + 1 => 'hydra:IriTemplateMapping', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 12 => [ + '@id' => 'hydra:readable', + '@type' => 'rdf:Property', + 'label' => 'readable', + 'comment' => 'True if the client can retrieve the property\'s value, false otherwise.', + 'domain' => 'hydra:SupportedProperty', + 'range' => 'xsd:boolean', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 13 => [ + '@id' => 'hydra:writable', + '@type' => 'rdf:Property', + 'label' => 'writable', + 'comment' => 'True if the client can change the property\'s value, false otherwise.', + 'domain' => 'hydra:SupportedProperty', + 'range' => 'xsd:boolean', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 14 => [ + '@id' => 'hydra:writeable', + 'subPropertyOf' => 'hydra:writable', + 'label' => 'writable', + 'comment' => 'This property is left for compatibility purposes and hydra:writable should be used instead.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'archaic', + ], + 15 => [ + '@id' => 'hydra:supportedOperation', + '@type' => 'hydra:Link', + 'label' => 'supported operation', + 'comment' => 'An operation supported by instances of the specific Hydra class, or the target of the Hydra link, or IRI template.', + 'range' => 'hydra:Operation', + 'domainIncludes' => [ + 0 => 'rdfs:Class', + 1 => 'hydra:Class', + 2 => 'hydra:Link', + 3 => 'hydra:TemplatedLink', + 4 => 'hydra:SupportedProperty', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 16 => [ + '@id' => 'hydra:operation', + '@type' => 'hydra:Link', + 'label' => 'operation', + 'comment' => 'An operation supported by the Hydra resource', + 'domain' => 'hydra:Resource', + 'range' => 'hydra:Operation', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 17 => [ + '@id' => 'hydra:Operation', + '@type' => 'hydra:Class', + 'label' => 'Operation', + 'comment' => 'An operation.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 18 => [ + '@id' => 'hydra:method', + '@type' => 'rdf:Property', + 'label' => 'method', + 'comment' => 'The HTTP method.', + 'domain' => 'hydra:Operation', + 'range' => 'xsd:string', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 19 => [ + '@id' => 'hydra:expects', + '@type' => 'hydra:Link', + 'label' => 'expects', + 'comment' => 'The information expected by the Web API.', + 'domain' => 'hydra:Operation', + 'rangeIncludes' => [ + 0 => 'rdfs:Resource', + 1 => 'hydra:Resource', + 2 => 'rdfs:Class', + 3 => 'hydra:Class', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 20 => [ + '@id' => 'hydra:returns', + '@type' => 'hydra:Link', + 'label' => 'returns', + 'comment' => 'The information returned by the Web API on success', + 'domain' => 'hydra:Operation', + 'rangeIncludes' => [ + 0 => 'rdfs:Resource', + 1 => 'hydra:Resource', + 2 => 'rdfs:Class', + 3 => 'hydra:Class', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 21 => [ + '@id' => 'hydra:Status', + '@type' => 'hydra:Class', + 'label' => 'Status code description', + 'comment' => 'Additional information about a status code that might be returned.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 22 => [ + '@id' => 'hydra:statusCode', + '@type' => 'rdf:Property', + 'label' => 'status code', + 'comment' => 'The HTTP status code. Please note it may happen this value will be different to actual status code received.', + 'domain' => 'hydra:Status', + 'range' => 'xsd:integer', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 23 => [ + '@id' => 'hydra:title', + '@type' => 'rdf:Property', + 'subPropertyOf' => 'rdfs:label', + 'label' => 'title', + 'comment' => 'A title, often used along with a description.', + 'range' => 'xsd:string', + 'domainIncludes' => [ + 0 => 'hydra:ApiDocumentation', + 1 => 'hydra:Status', + 2 => 'hydra:Class', + 3 => 'hydra:SupportedProperty', + 4 => 'hydra:Operation', + 5 => 'hydra:Link', + 6 => 'hydra:TemplatedLink', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 24 => [ + '@id' => 'hydra:description', + '@type' => 'rdf:Property', + 'subPropertyOf' => 'rdfs:comment', + 'label' => 'description', + 'comment' => 'A description.', + 'range' => 'xsd:string', + 'domainIncludes' => [ + 0 => 'hydra:ApiDocumentation', + 1 => 'hydra:Status', + 2 => 'hydra:Class', + 3 => 'hydra:SupportedProperty', + 4 => 'hydra:Operation', + 5 => 'hydra:Link', + 6 => 'hydra:TemplatedLink', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 25 => [ + '@id' => 'hydra:Error', + '@type' => 'hydra:Class', + 'subClassOf' => 'hydra:Status', + 'label' => 'Error', + 'comment' => 'A runtime error, used to report information beyond the returned status code.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 26 => [ + '@id' => 'hydra:Collection', + '@type' => 'hydra:Class', + 'subClassOf' => 'hydra:Resource', + 'label' => 'Collection', + 'comment' => 'A collection holding references to a number of related resources.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 27 => [ + '@id' => 'hydra:collection', + '@type' => 'hydra:Link', + 'label' => 'collection', + 'comment' => 'Collections somehow related to this resource.', + 'range' => 'hydra:Collection', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 28 => [ + '@id' => 'hydra:memberAssertion', + 'label' => 'member assertion', + 'comment' => 'Semantics of each member provided by the collection.', + 'domainIncludes' => [ + 'hydra:Collection', + 'hydra:Class', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 29 => [ + '@id' => 'hydra:manages', + 'subPropertyOf' => 'hydra:memberAssertion', + 'label' => 'manages', + 'comment' => 'This predicate is left for compatibility purposes and hydra:memberAssertion should be used instead.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'archaic', + ], + 30 => [ + '@id' => 'hydra:subject', + 'label' => 'subject', + 'comment' => 'The subject.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 31 => [ + '@id' => 'hydra:object', + 'label' => 'object', + 'comment' => 'The object.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 32 => [ + '@id' => 'hydra:member', + '@type' => 'hydra:Link', + 'label' => 'member', + 'comment' => 'A member of the collection', + 'domain' => 'hydra:Collection', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 33 => [ + '@id' => 'hydra:view', + '@type' => 'hydra:Link', + 'label' => 'view', + 'comment' => 'A specific view of a resource.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 34 => [ + '@id' => 'hydra:PartialCollectionView', + '@type' => 'hydra:Class', + 'subClassOf' => 'hydra:Resource', + 'label' => 'PartialCollectionView', + 'comment' => 'A PartialCollectionView describes a partial view of a Collection. Multiple PartialCollectionViews can be connected with the the next/previous properties to allow a client to retrieve all members of the collection.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 35 => [ + '@id' => 'hydra:totalItems', + '@type' => 'rdf:Property', + 'label' => 'total items', + 'comment' => 'The total number of items referenced by a collection.', + 'domain' => 'hydra:Collection', + 'range' => 'xsd:integer', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 36 => [ + '@id' => 'hydra:first', + '@type' => 'hydra:Link', + 'label' => 'first', + 'comment' => 'The first resource of an interlinked set of resources.', + 'domain' => 'hydra:Resource', + 'range' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 37 => [ + '@id' => 'hydra:last', + '@type' => 'hydra:Link', + 'label' => 'last', + 'comment' => 'The last resource of an interlinked set of resources.', + 'domain' => 'hydra:Resource', + 'range' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 38 => [ + '@id' => 'hydra:next', + '@type' => 'hydra:Link', + 'label' => 'next', + 'comment' => 'The resource following the current instance in an interlinked set of resources.', + 'domain' => 'hydra:Resource', + 'range' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 39 => [ + '@id' => 'hydra:previous', + '@type' => 'hydra:Link', + 'label' => 'previous', + 'comment' => 'The resource preceding the current instance in an interlinked set of resources.', + 'domain' => 'hydra:Resource', + 'range' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 40 => [ + '@id' => 'hydra:search', + '@type' => 'hydra:TemplatedLink', + 'label' => 'search', + 'comment' => 'A IRI template that can be used to query a collection.', + 'range' => 'hydra:IriTemplate', + 'domain' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 41 => [ + '@id' => 'hydra:freetextQuery', + '@type' => 'rdf:Property', + 'label' => 'freetext query', + 'comment' => 'A property representing a freetext query.', + 'range' => 'xsd:string', + 'domain' => 'hydra:Resource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 42 => [ + '@id' => 'hydra:TemplatedLink', + '@type' => 'hydra:Class', + 'subClassOf' => [ + 0 => 'hydra:Resource', + 1 => 'rdf:Property', + ], + 'label' => 'Templated Link', + 'comment' => 'A templated link.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 43 => [ + '@id' => 'hydra:IriTemplate', + '@type' => 'hydra:Class', + 'label' => 'IRI Template', + 'comment' => 'The class of IRI templates.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 44 => [ + '@id' => 'hydra:template', + '@type' => 'rdf:Property', + 'label' => 'template', + 'comment' => 'A templated string with placeholders. The literal\'s datatype indicates the template syntax; if not specified, hydra:Rfc6570Template is assumed.', + 'seeAlso' => 'hydra:Rfc6570Template', + 'domain' => 'hydra:IriTemplate', + 'range' => 'hydra:Rfc6570Template', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 45 => [ + '@id' => 'hydra:Rfc6570Template', + '@type' => 'rdfs:Datatype', + 'label' => 'RFC6570 IRI template', + 'comment' => 'An IRI template as defined by RFC6570.', + 'seeAlso' => 'http://tools.ietf.org/html/rfc6570', + 'range' => 'xsd:string', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 46 => [ + '@id' => 'hydra:variableRepresentation', + '@type' => 'rdf:Property', + 'label' => 'variable representation', + 'comment' => 'The representation format to use when expanding the IRI template.', + 'range' => 'hydra:VariableRepresentation', + 'domainIncludes' => [ + 'hydra:IriTemplateMapping', + 'hydra:IriTemplate', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 47 => [ + '@id' => 'hydra:VariableRepresentation', + '@type' => 'hydra:Class', + 'label' => 'VariableRepresentation', + 'comment' => 'A representation specifies how to serialize variable values into strings.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 48 => [ + '@id' => 'hydra:BasicRepresentation', + '@type' => 'hydra:VariableRepresentation', + 'label' => 'BasicRepresentation', + 'comment' => 'A representation that serializes just the lexical form of a variable value, but omits language and type information.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 49 => [ + '@id' => 'hydra:ExplicitRepresentation', + '@type' => 'hydra:VariableRepresentation', + 'label' => 'ExplicitRepresentation', + 'comment' => 'A representation that serializes a variable value including its language and type information and thus differentiating between IRIs and literals.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 50 => [ + '@id' => 'hydra:mapping', + '@type' => 'rdf:Property', + 'label' => 'mapping', + 'comment' => 'A variable-to-property mapping of the IRI template.', + 'domain' => 'hydra:IriTemplate', + 'range' => 'hydra:IriTemplateMapping', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 51 => [ + '@id' => 'hydra:IriTemplateMapping', + '@type' => 'hydra:Class', + 'label' => 'IriTemplateMapping', + 'comment' => 'A mapping from an IRI template variable to a property.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 52 => [ + '@id' => 'hydra:variable', + '@type' => 'rdf:Property', + 'label' => 'variable', + 'comment' => 'An IRI template variable', + 'domain' => 'hydra:IriTemplateMapping', + 'range' => 'xsd:string', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 53 => [ + '@id' => 'hydra:resolveRelativeUsing', + '@type' => 'rdf:Property', + 'label' => 'relative Uri resolution', + 'domain' => 'hydra:IriTemplate', + 'range' => 'hydra:BaseUriSource', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 54 => [ + '@id' => 'hydra:BaseUriSource', + '@type' => 'hydra:Class', + 'subClassOf' => 'hydra:Resource', + 'label' => 'Base Uri source', + 'comment' => 'Provides a base abstract for base Uri source for Iri template resolution.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 55 => [ + '@id' => 'hydra:Rfc3986', + '@type' => 'hydra:BaseUriSource', + 'label' => 'RFC 3986 based', + 'comment' => 'States that the base Uri should be established using RFC 3986 reference resolution algorithm specified in section 5.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 56 => [ + '@id' => 'hydra:LinkContext', + '@type' => 'hydra:BaseUriSource', + 'label' => 'Link context', + 'comment' => 'States that the link\'s context IRI, as defined in RFC 5988, should be used as the base Uri', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 57 => [ + '@id' => 'hydra:offset', + '@type' => 'rdf:Property', + 'label' => 'skip', + 'comment' => 'Instructs to skip N elements of the set.', + 'range' => 'xsd:nonNegativeInteger', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 58 => [ + '@id' => 'hydra:limit', + '@type' => 'rdf:Property', + 'label' => 'take', + 'comment' => 'Instructs to limit set only to N elements.', + 'range' => 'xsd:nonNegativeInteger', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 59 => [ + '@id' => 'hydra:pageIndex', + '@type' => 'rdf:Property', + 'subPropertyOf' => 'hydra:pageReference', + 'label' => 'page index', + 'comment' => 'Instructs to provide a specific page of the collection at a given index.', + 'range' => 'xsd:nonNegativeInteger', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 60 => [ + '@id' => 'hydra:pageReference', + '@type' => 'rdf:Property', + 'label' => 'page reference', + 'comment' => 'Instructs to provide a specific page reference of the collection.', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 61 => [ + '@id' => 'hydra:returnsHeader', + '@type' => 'rdf:Property', + 'label' => 'returns header', + 'comment' => 'Name of the header returned by the operation.', + 'domain' => 'hydra:Operation', + 'rangeIncludes' => [ + 0 => 'xsd:string', + 1 => 'hydra:HeaderSpecification', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 62 => [ + '@id' => 'hydra:expectsHeader', + '@type' => 'rdf:Property', + 'label' => 'expects header', + 'comment' => 'Specification of the header expected by the operation.', + 'domain' => 'hydra:Operation', + 'rangeIncludes' => [ + 0 => 'xsd:string', + 1 => 'hydra:HeaderSpecification', + ], + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 63 => [ + '@id' => 'hydra:HeaderSpecification', + '@type' => 'rdfs:Class', + 'subClassOf' => 'hydra:Resource', + 'label' => 'Header specification', + 'comment' => 'Specifies a possible either expected or returned header values', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 64 => [ + '@id' => 'hydra:headerName', + '@type' => 'rdf:Property', + 'label' => 'header name', + 'comment' => 'Name of the header.', + 'domain' => 'hydra:HeaderSpecification', + 'range' => 'xsd:string', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 65 => [ + '@id' => 'hydra:possibleValue', + '@type' => 'rdf:Property', + 'label' => 'possible header value', + 'comment' => 'Possible value of the header.', + 'domain' => 'hydra:HeaderSpecification', + 'range' => 'xsd:string', + 'vs:term_status' => 'testing', + ], + 66 => [ + '@id' => 'hydra:closedSet', + '@type' => 'rdf:Property', + 'label' => 'closed set', + 'comment' => 'Determines whether the provided set of header values is closed or not.', + 'domain' => 'hydra:HeaderSpecification', + 'range' => 'xsd:boolean', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + 67 => [ + '@id' => 'hydra:extension', + '@type' => 'rdf:Property', + 'label' => 'extension', + 'comment' => 'Hint on what kind of extensions are in use.', + 'domain' => 'hydra:ApiDocumentation', + 'isDefinedBy' => 'http://www.w3.org/ns/hydra/core', + 'vs:term_status' => 'testing', + ], + ], +]; diff --git a/src/JsonLd/JsonStreamer/ValueTransformer/ContextValueTransformer.php b/src/JsonLd/JsonStreamer/ValueTransformer/ContextValueTransformer.php new file mode 100644 index 00000000000..fbf75692828 --- /dev/null +++ b/src/JsonLd/JsonStreamer/ValueTransformer/ContextValueTransformer.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonLd\JsonStreamer\ValueTransformer; + +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface; +use Symfony\Component\TypeInfo\Type; + +final class ContextValueTransformer implements ValueTransformerInterface +{ + public function __construct( + private readonly UrlGeneratorInterface $urlGenerator, + ) { + } + + public function transform(mixed $value, array $options = []): mixed + { + if (!isset($options['operation'])) { + throw new RuntimeException('Operation is not defined'); + } + + return $this->urlGenerator->generate('api_jsonld_context', ['shortName' => $options['operation']->getShortName()], $options['operation']->getUrlGenerationStrategy()); + } + + public static function getStreamValueType(): Type + { + return Type::string(); + } +} diff --git a/src/JsonLd/JsonStreamer/ValueTransformer/IriValueTransformer.php b/src/JsonLd/JsonStreamer/ValueTransformer/IriValueTransformer.php new file mode 100644 index 00000000000..383ed920c8a --- /dev/null +++ b/src/JsonLd/JsonStreamer/ValueTransformer/IriValueTransformer.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonLd\JsonStreamer\ValueTransformer; + +use ApiPlatform\Hydra\Collection; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface; +use Symfony\Component\TypeInfo\Type; + +final class IriValueTransformer implements ValueTransformerInterface +{ + public function __construct( + private readonly IriConverterInterface $iriConverter, + ) { + } + + public function transform(mixed $value, array $options = []): mixed + { + if (!isset($options['operation'])) { + throw new RuntimeException('Operation is not defined'); + } + + if ($options['_current_object'] instanceof Collection) { + return $this->iriConverter->getIriFromResource($options['operation']->getClass(), UrlGeneratorInterface::ABS_PATH, $options['operation']); + } + + return $this->iriConverter->getIriFromResource( + $options['_current_object'], + UrlGeneratorInterface::ABS_PATH, + $options['operation'] instanceof CollectionOperationInterface ? null : $options['operation'], + ); + } + + public static function getStreamValueType(): Type + { + return Type::string(); + } +} diff --git a/src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php b/src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php new file mode 100644 index 00000000000..238a19d72b4 --- /dev/null +++ b/src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonLd\JsonStreamer\ValueTransformer; + +use ApiPlatform\Hydra\Collection; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface; +use Symfony\Component\TypeInfo\Type; + +final class TypeValueTransformer implements ValueTransformerInterface +{ + public function __construct( + private readonly ResourceClassResolverInterface $resourceClassResolver, + private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, + ) { + } + + public function transform(mixed $value, array $options = []): mixed + { + if ($options['_current_object'] instanceof Collection) { + return 'Collection'; + } + + $dataClass = isset($options['data']) && \is_object($options['data']) ? $options['data']::class : null; + if (($currentClass = $options['_current_object']::class) === $dataClass) { + if (!isset($options['operation'])) { + throw new RuntimeException('Operation is not defined'); + } + + return $this->getOperationType($options['operation']); + } + + if (!$this->resourceClassResolver->isResourceClass($currentClass)) { + return null; + } + + /** @var HttpOperation $op */ + $op = $this->resourceMetadataCollectionFactory->create($currentClass)->getOperation(httpOperation: true); + + return $this->getOperationType($op); + } + + public static function getStreamValueType(): Type + { + return Type::string(); + } + + private function getOperationType(HttpOperation $operation): array|string + { + if (($t = $operation->getTypes()) && 1 === \count($t)) { + return $operation->getTypes()[0]; + } + + return $t ?: $operation->getShortname(); + } +} diff --git a/src/JsonLd/JsonStreamer/WritePropertyMetadataLoader.php b/src/JsonLd/JsonStreamer/WritePropertyMetadataLoader.php new file mode 100644 index 00000000000..147be2ee14d --- /dev/null +++ b/src/JsonLd/JsonStreamer/WritePropertyMetadataLoader.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonLd\JsonStreamer; + +use ApiPlatform\Hydra\Collection; +use ApiPlatform\Hydra\IriTemplate; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\TypeHelper; +use Symfony\Component\JsonStreamer\Mapping\PropertyMetadata; +use Symfony\Component\JsonStreamer\Mapping\PropertyMetadataLoaderInterface; +use Symfony\Component\TypeInfo\Type; + +final class WritePropertyMetadataLoader implements PropertyMetadataLoaderInterface +{ + public function __construct( + private readonly PropertyMetadataLoaderInterface $loader, + private readonly ResourceClassResolverInterface $resourceClassResolver, + private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, + private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, + ) { + } + + public function load(string $className, array $options = [], array $context = []): array + { + $properties = $this->loader->load($className, $options, $context); + + if (IriTemplate::class === $className) { + $properties['template'] = new PropertyMetadata( + 'template', + Type::string(), + ['api_platform.hydra.json_streamer.write.value_transformer.template'], + ); + + return $properties; + } + + if (Collection::class !== $className && !$this->resourceClassResolver->isResourceClass($className)) { + return $properties; + } + + $originalClassName = TypeHelper::getClassName($context['original_type']); + $hasIri = true; + $virtualProperty = 'id'; + + foreach ($this->propertyNameCollectionFactory->create($originalClassName) as $property) { + $propertyMetadata = $this->propertyMetadataFactory->create($originalClassName, $property); + if ($propertyMetadata->isIdentifier()) { + $virtualProperty = $property; + } + + if ($className === $originalClassName) { + continue; + } + + if ($propertyMetadata->getNativeType()->isIdentifiedBy($className)) { + $hasIri = $propertyMetadata->getGenId(); + $virtualProperty = iterator_to_array($this->propertyNameCollectionFactory->create($className))[0]; + } + } + + if ($hasIri) { + $properties['@id'] = new PropertyMetadata( + $virtualProperty, // virtual property + Type::mixed(), // virtual property + ['api_platform.jsonld.json_streamer.write.value_transformer.iri'], + ); + } + + $properties['@type'] = new PropertyMetadata( + $virtualProperty, // virtual property + Type::mixed(), // virtual property + ['api_platform.jsonld.json_streamer.write.value_transformer.type'], + ); + + if ($className !== $originalClassName) { + return $properties; + } + + if (Collection::class === $originalClassName || ($this->resourceClassResolver->isResourceClass($originalClassName) && !isset($context['generated_classes'][Collection::class]))) { + $properties['@context'] = new PropertyMetadata( + $virtualProperty, // virual property + Type::string(), // virtual property + ['api_platform.jsonld.json_streamer.write.value_transformer.context'], + ); + } + + return $properties; + } +} diff --git a/src/JsonLd/README.md b/src/JsonLd/README.md new file mode 100644 index 00000000000..2e243daa495 --- /dev/null +++ b/src/JsonLd/README.md @@ -0,0 +1,14 @@ +# API Platform - JSON-LD + +The [JSON-LD](https://json-ld.org/) component of the [API Platform](https://api-platform.com) framework. + +Data is messy and disconnected. JSON-LD organizes and connects it, creating a better Web. + +[Documentation](https://api-platform.com/docs/core/extending-jsonld-context/) + +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/JsonLd/Serializer/ErrorNormalizer.php b/src/JsonLd/Serializer/ErrorNormalizer.php new file mode 100644 index 00000000000..1227421a5c3 --- /dev/null +++ b/src/JsonLd/Serializer/ErrorNormalizer.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonLd\Serializer; + +use ApiPlatform\State\ApiResource\Error; +use ApiPlatform\Validator\Exception\ValidationException; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class ErrorNormalizer implements NormalizerInterface +{ + use HydraPrefixTrait; + + public function __construct(private readonly NormalizerInterface $inner, private readonly array $defaultContext = []) + { + } + + public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null + { + $context += $this->defaultContext; + $normalized = $this->inner->normalize($object, $format, $context); + $hydraPrefix = $this->getHydraPrefix($context); + if (!$hydraPrefix) { + return $normalized; + } + + if ('Error' === $normalized['@type']) { + $normalized['@type'] = 'hydra:Error'; + } + + if (isset($normalized['description'])) { + $normalized['hydra:description'] = $normalized['description']; + unset($normalized['description']); + } + + if (isset($normalized['title'])) { + $normalized['hydra:title'] = $normalized['title']; + // this is confusing as the field is also available in the Problem Detail Json representation + // but we don't want to repeat the title in the response, tldr: use hydra without prefix + unset($normalized['title']); + } + + return $normalized; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $this->inner->supportsNormalization($data, $format, $context) + && (is_a($data, Error::class) || is_a($data, ValidationException::class)); + } + + public function getSupportedTypes(?string $format): array + { + if (method_exists($this->inner, 'getSupportedTypes')) { + return $this->inner->getSupportedTypes($format); + } + + return []; + } +} diff --git a/src/JsonLd/Serializer/HydraPrefixTrait.php b/src/JsonLd/Serializer/HydraPrefixTrait.php new file mode 100644 index 00000000000..fec9cd0952f --- /dev/null +++ b/src/JsonLd/Serializer/HydraPrefixTrait.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonLd\Serializer; + +use ApiPlatform\JsonLd\ContextBuilder; + +trait HydraPrefixTrait +{ + /** + * @param array $context + */ + private function getHydraPrefix(array $context): string + { + return ($context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX] ?? true) ? ContextBuilder::HYDRA_PREFIX : ''; + } +} diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index a64b3d7f8a4..5ee28e87d41 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -13,8 +13,6 @@ namespace ApiPlatform\JsonLd\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\JsonLd\AnonymousContextBuilderInterface; use ApiPlatform\JsonLd\ContextBuilderInterface; use ApiPlatform\Metadata\Exception\ItemNotFoundException; @@ -72,7 +70,7 @@ final class ItemNormalizer extends AbstractItemNormalizer '@vocab', ]; - public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface|LegacyIriConverterInterface $iriConverter, ResourceClassResolverInterface|LegacyResourceClassResolverInterface $resourceClassResolver, private readonly ContextBuilderInterface $contextBuilder, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) + public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, private readonly ContextBuilderInterface $contextBuilder, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceAccessCheckerInterface $resourceAccessChecker = null, protected ?TagCollectorInterface $tagCollector = null) { parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector); } @@ -85,6 +83,9 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { return self::FORMAT === $format ? parent::getSupportedTypes($format) : []; @@ -113,20 +114,24 @@ public function normalize(mixed $object, ?string $format = null, array $context } elseif ($this->contextBuilder instanceof AnonymousContextBuilderInterface) { if ($context['api_collection_sub_level'] ?? false) { unset($context['api_collection_sub_level']); - $context['output']['genid'] = true; + $context['output']['gen_id'] ??= true; $context['output']['iri'] = null; } + if ($this->resourceClassResolver->isResourceClass($resourceClass)) { + $context['output']['operation'] = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(); + } + // We should improve what's behind the context creation, its probably more complicated then it should $metadata = $this->createJsonLdContext($this->contextBuilder, $object, $context); } - // maybe not needed anymore - if (isset($context['operation']) && $previousResourceClass !== $resourceClass) { - unset($context['operation'], $context['operation_name']); + // Special case: non-resource got serialized and contains a resource therefore we need to reset part of the context + if ($previousResourceClass !== $resourceClass) { + unset($context['operation'], $context['operation_name'], $context['output']); } - if (true === ($context['force_iri_generation'] ?? true) && $iri = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context)) { + if (true === ($context['output']['gen_id'] ?? true) && true === ($context['force_iri_generation'] ?? true) && $iri = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $context['operation'] ?? null, $context)) { $context['iri'] = $iri; $metadata['@id'] = $iri; } @@ -138,9 +143,12 @@ public function normalize(mixed $object, ?string $format = null, array $context return $data; } - if (!isset($metadata['@type']) && $isResourceClass) { - $operation = $context['operation'] ?? $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(); + $operation = $context['operation'] ?? null; + if ($isResourceClass && !$operation) { + $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(); + } + if (!isset($metadata['@type']) && $operation) { $types = $operation instanceof HttpOperation ? $operation->getTypes() : null; if (null === $types) { $types = [$operation->getShortName()]; @@ -177,7 +185,7 @@ public function denormalize(mixed $data, string $class, ?string $format = null, } catch (ItemNotFoundException $e) { $operation = $context['operation'] ?? null; - if (!('PUT' === $operation?->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? false))) { + if (!('PUT' === $operation?->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? true))) { throw $e; } } diff --git a/src/JsonLd/Serializer/JsonLdContextTrait.php b/src/JsonLd/Serializer/JsonLdContextTrait.php index edae4b21d10..79008dfe41c 100644 --- a/src/JsonLd/Serializer/JsonLdContextTrait.php +++ b/src/JsonLd/Serializer/JsonLdContextTrait.php @@ -14,8 +14,8 @@ namespace ApiPlatform\JsonLd\Serializer; use ApiPlatform\JsonLd\AnonymousContextBuilderInterface; +use ApiPlatform\JsonLd\ContextBuilder; use ApiPlatform\JsonLd\ContextBuilderInterface; -use ApiPlatform\Metadata\Error; /** * Creates and manipulates the Serializer context. @@ -43,24 +43,26 @@ private function addJsonLdContext(ContextBuilderInterface $contextBuilder, strin return $data; } - if (($operation = $context['operation'] ?? null) && ($operation->getExtraProperties()['rfc_7807_compliant_errors'] ?? false) && $operation instanceof Error) { - return $data; - } - $data['@context'] = $contextBuilder->getResourceContextUri($resourceClass); return $data; } - private function createJsonLdContext(AnonymousContextBuilderInterface $contextBuilder, $object, array &$context): array + private function createJsonLdContext(AnonymousContextBuilderInterface $contextBuilder, object $object, array &$context): array { + $anonymousContext = ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null]; + + if (isset($context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX])) { + $anonymousContext[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX] = $context[ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX]; + } + // We're in a collection, don't add the @context part if (isset($context['jsonld_has_context'])) { - return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null, 'has_context' => true]); + return $contextBuilder->getAnonymousResourceContext($object, ['has_context' => true] + $anonymousContext); } $context['jsonld_has_context'] = true; - return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null]); + return $contextBuilder->getAnonymousResourceContext($object, $anonymousContext); } } diff --git a/src/JsonLd/Serializer/ObjectNormalizer.php b/src/JsonLd/Serializer/ObjectNormalizer.php index 5339e520728..23659458c91 100644 --- a/src/JsonLd/Serializer/ObjectNormalizer.php +++ b/src/JsonLd/Serializer/ObjectNormalizer.php @@ -13,26 +13,22 @@ namespace ApiPlatform\JsonLd\Serializer; -use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; -use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\JsonLd\AnonymousContextBuilderInterface; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\IriConverterInterface; -use ApiPlatform\Serializer\CacheableSupportsMethodInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface as BaseCacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use Symfony\Component\Serializer\Serializer; /** * Decorates the output with JSON-LD metadata when appropriate, but otherwise just * passes through to the decorated normalizer. */ -final class ObjectNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface +final class ObjectNormalizer implements NormalizerInterface { use JsonLdContextTrait; public const FORMAT = 'jsonld'; - public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private AnonymousContextBuilderInterface $anonymousContextBuilder) + public function __construct(private readonly NormalizerInterface $decorated, private readonly IriConverterInterface $iriConverter, private AnonymousContextBuilderInterface $anonymousContextBuilder) { } @@ -44,32 +40,14 @@ public function supportsNormalization(mixed $data, ?string $format = null, array return self::FORMAT === $format && $this->decorated->supportsNormalization($data, $format, $context); } + /** + * @param string|null $format + */ public function getSupportedTypes($format): array { - // @deprecated remove condition when support for symfony versions under 6.3 is dropped - if (!method_exists($this->decorated, 'getSupportedTypes')) { - return [ - '*' => $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(), - ]; - } - return self::FORMAT === $format ? $this->decorated->getSupportedTypes($format) : []; } - public function hasCacheableSupportsMethod(): bool - { - if (method_exists(Serializer::class, 'getSupportedTypes')) { - trigger_deprecation( - 'api-platform/core', - '3.1', - 'The "%s()" method is deprecated, use "getSupportedTypes()" instead.', - __METHOD__ - ); - } - - return $this->decorated instanceof BaseCacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(); - } - /** * {@inheritdoc} */ diff --git a/src/JsonLd/composer.json b/src/JsonLd/composer.json index e57022afecb..d9bd0cb4c66 100644 --- a/src/JsonLd/composer.json +++ b/src/JsonLd/composer.json @@ -7,11 +7,7 @@ "GraphQL", "API", "JSON-LD", - "Hydra", - "JSONAPI", - "OpenAPI", - "HAL", - "Swagger" + "Hydra" ], "homepage": "https://api-platform.com/", "license": "MIT", @@ -27,15 +23,18 @@ } ], "require": { - "php": ">=8.1", - "api-platform/state": "*@dev || ^3.1", - "api-platform/metadata": "*@dev || ^3.1", - "api-platform/serializer": "*@dev || ^3.1" + "php": ">=8.2", + "api-platform/state": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/serializer": "^4.1.11" }, "autoload": { "psr-4": { "ApiPlatform\\JsonLd\\": "" }, + "files": [ + "./HydraContext.php" + ], "exclude-from-classmap": [ "/Tests/" ] @@ -52,13 +51,30 @@ }, "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" }, "symfony": { - "require": "^6.1" + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" } }, "scripts": { "test": "./vendor/bin/phpunit" - } + }, + "require-dev": { + "symfony/type-info": "^7.3", + "phpunit/phpunit": "11.5.x-dev" + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] } diff --git a/src/JsonSchema/.gitattributes b/src/JsonSchema/.gitattributes index ae3c2e1685a..801f2080d71 100644 --- a/src/JsonSchema/.gitattributes +++ b/src/JsonSchema/.gitattributes @@ -1,2 +1,5 @@ +/.github export-ignore +/.gitattributes export-ignore /.gitignore export-ignore /Tests export-ignore +/phpunit.xml.dist export-ignore diff --git a/src/JsonSchema/.github/workflows/close_pr.yml b/src/JsonSchema/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/JsonSchema/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/JsonSchema/Command/JsonSchemaGenerateCommand.php b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php index eebcf5d5286..7b2f282146f 100644 --- a/src/JsonSchema/Command/JsonSchemaGenerateCommand.php +++ b/src/JsonSchema/Command/JsonSchemaGenerateCommand.php @@ -16,6 +16,7 @@ use ApiPlatform\JsonSchema\Schema; use ApiPlatform\JsonSchema\SchemaFactoryInterface; use ApiPlatform\Metadata\HttpOperation; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Input\InputArgument; @@ -29,6 +30,7 @@ * * @author Jacques Lefebvre */ +#[AsCommand(name: 'api:json-schema:generate')] final class JsonSchemaGenerateCommand extends Command { private array $formats; @@ -90,9 +92,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int return 0; } - - public static function getDefaultName(): string - { - return 'api:json-schema:generate'; - } } diff --git a/src/JsonSchema/DefinitionNameFactory.php b/src/JsonSchema/DefinitionNameFactory.php index 9cfbb7deb07..fe56b63e540 100644 --- a/src/JsonSchema/DefinitionNameFactory.php +++ b/src/JsonSchema/DefinitionNameFactory.php @@ -24,8 +24,11 @@ final class DefinitionNameFactory implements DefinitionNameFactoryInterface private const GLUE = '.'; private array $prefixCache = []; - public function __construct(private ?array $distinctFormats) + public function __construct(private ?array $distinctFormats = null) { + if ($distinctFormats) { + trigger_deprecation('api-platform/json-schema', '4.2', 'The distinctFormats argument is deprecated and will be removed in 5.0.'); + } } public function create(string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?Operation $operation = null, array $serializerContext = []): string @@ -44,19 +47,26 @@ public function create(string $className, string $format = 'json', ?string $inpu $prefix .= self::GLUE.$shortName; } - if ('json' !== $format && ($this->distinctFormats[$format] ?? false)) { + // TODO: remove in 5.0 + $v = $this->distinctFormats ? ($this->distinctFormats[$format] ?? false) : true; + + if ('json' !== $format && $v) { // JSON is the default, and so isn't included in the definition name $prefix .= self::GLUE.$format; } $definitionName = $serializerContext[SchemaFactory::OPENAPI_DEFINITION_NAME] ?? null; - if ($definitionName) { - $name = \sprintf('%s-%s', $prefix, $definitionName); + if (null !== $definitionName) { + $name = \sprintf('%s%s', $prefix, $definitionName ? '-'.$definitionName : $definitionName); } else { $groups = (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []); $name = $groups ? \sprintf('%s-%s', $prefix, implode('_', $groups)) : $prefix; } + if (false === ($serializerContext['gen_id'] ?? true)) { + $name .= '_noid'; + } + return $this->encodeDefinitionName($name); } diff --git a/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php b/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php index d5070f8aecc..cf41fbed56e 100644 --- a/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php +++ b/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php @@ -13,15 +13,24 @@ namespace ApiPlatform\JsonSchema\Metadata\Property\Factory; -use ApiPlatform\Exception\PropertyNotFoundException; use ApiPlatform\JsonSchema\Schema; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Exception\PropertyNotFoundException; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; use Doctrine\Common\Collections\ArrayCollection; use Ramsey\Uuid\UuidInterface; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\IntersectionType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\Type\UnionType; +use Symfony\Component\TypeInfo\Type\WrappingTypeInterface; +use Symfony\Component\TypeInfo\TypeIdentifier; use Symfony\Component\Uid\Ulid; use Symfony\Component\Uid\Uuid; @@ -34,8 +43,10 @@ final class SchemaPropertyMetadataFactory implements PropertyMetadataFactoryInte public const JSON_SCHEMA_USER_DEFINED = 'user_defined_schema'; - public function __construct(ResourceClassResolverInterface $resourceClassResolver, private readonly ?PropertyMetadataFactoryInterface $decorated = null) - { + public function __construct( + ResourceClassResolverInterface $resourceClassResolver, + private readonly ?PropertyMetadataFactoryInterface $decorated = null, + ) { $this->resourceClassResolver = $resourceClassResolver; } @@ -51,7 +62,7 @@ public function create(string $resourceClass, string $property, array $options = } } - $extraProperties = $propertyMetadata->getExtraProperties() ?? []; + $extraProperties = $propertyMetadata->getExtraProperties(); // see AttributePropertyMetadataFactory if (true === ($extraProperties[self::JSON_SCHEMA_USER_DEFINED] ?? false)) { // schema seems to have been declared by the user: do not override nor complete user value @@ -84,36 +95,290 @@ public function create(string $resourceClass, string $property, array $options = $propertySchema['externalDocs'] = ['url' => $iri]; } - $types = $propertyMetadata->getBuiltinTypes() ?? []; + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + return $propertyMetadata->withSchema($this->getLegacyTypeSchema($propertyMetadata, $propertySchema, $resourceClass, $property, $link)); + } + + return $propertyMetadata->withSchema($this->getTypeSchema($propertyMetadata, $propertySchema, $link)); + } + + private function getTypeSchema(ApiProperty $propertyMetadata, array $propertySchema, ?bool $link): array + { + $type = $propertyMetadata->getNativeType(); + + $className = null; + $typeIsResourceClass = function (Type $type) use (&$className): bool { + return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName()); + }; + $isResourceClass = $type?->isSatisfiedBy($typeIsResourceClass); - if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && (!\count($types) || null === ($className = $types[0]->getClassName()) || !$this->isResourceClass($className))) { + if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) && !$className) { + $propertySchema['readOnly'] = true; + } + + if (!\array_key_exists('default', $propertySchema) && null !== ($default = $propertyMetadata->getDefault()) && false === (\is_array($default) && empty($default)) && !$isResourceClass) { if ($default instanceof \BackedEnum) { $default = $default->value; } $propertySchema['default'] = $default; } - if (!\array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) { + if (!\array_key_exists('example', $propertySchema) && null !== ($example = $propertyMetadata->getExample()) && false === (\is_array($example) && empty($example))) { $propertySchema['example'] = $example; } - if (!\array_key_exists('example', $propertySchema) && \array_key_exists('default', $propertySchema)) { - $propertySchema['example'] = $propertySchema['default']; + $hasType = $this->getSchemaValue($propertySchema, 'type') || $this->getSchemaValue($propertyMetadata->getJsonSchemaContext() ?? [], 'type') || $this->getSchemaValue($propertyMetadata->getOpenapiContext() ?? [], 'type'); + $hasRef = $this->getSchemaValue($propertySchema, '$ref') || $this->getSchemaValue($propertyMetadata->getJsonSchemaContext() ?? [], '$ref') || $this->getSchemaValue($propertyMetadata->getOpenapiContext() ?? [], '$ref'); + + // never override the following keys if at least one is already set or if there's a custom openapi context + if ($hasType || $hasRef || !$type) { + return $propertySchema; + } + + if ($type instanceof CollectionType && null !== $propertyMetadata->getUriTemplate()) { + $type = $type->getCollectionValueType(); + } + + return $propertySchema + $this->getJsonSchemaFromType($type, $link); + } + + /** + * Applies nullability rules to a generated JSON schema based on the original type's nullability. + * + * @param array $schema the base JSON schema generated for the non-null type + * @param bool $isNullable whether the original type allows null + * + * @return array the JSON schema with nullability applied + */ + private function applyNullability(array $schema, bool $isNullable): array + { + if (!$isNullable) { + return $schema; + } + + if (isset($schema['type']) && 'null' === $schema['type'] && 1 === \count($schema)) { + return $schema; + } + + if (isset($schema['anyOf']) && \is_array($schema['anyOf'])) { + $hasNull = false; + foreach ($schema['anyOf'] as $anyOfSchema) { + if (isset($anyOfSchema['type']) && 'null' === $anyOfSchema['type']) { + $hasNull = true; + break; + } + } + if (!$hasNull) { + $schema['anyOf'][] = ['type' => 'null']; + } + + return $schema; + } + + if (isset($schema['type'])) { + $currentType = $schema['type']; + $schema['type'] = \is_array($currentType) ? array_merge($currentType, ['null']) : [$currentType, 'null']; + + if (isset($schema['enum'])) { + $schema['enum'][] = null; + + return $schema; + } + + return $schema; + } + + return ['anyOf' => [$schema, ['type' => 'null']]]; + } + + /** + * Converts a TypeInfo Type into a JSON Schema definition array. + * + * @return array + */ + private function getJsonSchemaFromType(Type $type, ?bool $readableLink = null): array + { + $isNullable = $type->isNullable(); + + if ($type instanceof UnionType) { + $subTypes = array_filter($type->getTypes(), fn ($t) => !($t instanceof BuiltinType && $t->isIdentifiedBy(TypeIdentifier::NULL))); + + foreach ($subTypes as $t) { + $s = $this->getJsonSchemaFromType($t, $readableLink); + // We can not find what type this is, let it be computed at runtime by the SchemaFactory + if (($s['type'] ?? null) === Schema::UNKNOWN_TYPE) { + return $s; + } + } + + $schemas = array_map(fn ($t) => $this->getJsonSchemaFromType($t, $readableLink), $subTypes); + + if (0 === \count($schemas)) { + $schema = []; + } elseif (1 === \count($schemas)) { + $schema = current($schemas); + } else { + $schema = ['anyOf' => array_values($schemas)]; + } + + return $this->applyNullability($schema, $isNullable); + } + + if ($type instanceof IntersectionType) { + $schemas = []; + foreach ($type->getTypes() as $t) { + while ($t instanceof WrappingTypeInterface) { + $t = $t->getWrappedType(); + } + + $subSchema = $this->getJsonSchemaFromType($t, $readableLink); + if (!empty($subSchema)) { + $schemas[] = $subSchema; + } + } + + return $this->applyNullability(['allOf' => $schemas], $isNullable); + } + + if ($type instanceof CollectionType) { + $valueType = $type->getCollectionValueType(); + $valueSchema = $this->getJsonSchemaFromType($valueType, $readableLink); + $keyType = $type->getCollectionKeyType(); + + // Associative array (string keys) + if ($keyType->isSatisfiedBy(fn (Type $t) => $t instanceof BuiltinType && $t->isIdentifiedBy(TypeIdentifier::INT))) { + $schema = [ + 'type' => 'array', + 'items' => $valueSchema, + ]; + } else { // List (int keys) + $schema = [ + 'type' => 'object', + 'additionalProperties' => $valueSchema, + ]; + } + + return $this->applyNullability($schema, $isNullable); + } + + if ($type instanceof ObjectType) { + $schema = $this->getClassSchemaDefinition($type->getClassName(), $readableLink); + + return $this->applyNullability($schema, $isNullable); + } + + if ($type instanceof BuiltinType) { + $schema = match ($type->getTypeIdentifier()) { + TypeIdentifier::INT => ['type' => 'integer'], + TypeIdentifier::FLOAT => ['type' => 'number'], + TypeIdentifier::BOOL => ['type' => 'boolean'], + TypeIdentifier::TRUE => ['type' => 'boolean', 'const' => true], + TypeIdentifier::FALSE => ['type' => 'boolean', 'const' => false], + TypeIdentifier::STRING => ['type' => 'string'], + TypeIdentifier::ARRAY => ['type' => 'array', 'items' => []], + TypeIdentifier::ITERABLE => ['type' => 'array', 'items' => []], + TypeIdentifier::OBJECT => ['type' => 'object'], + TypeIdentifier::RESOURCE => ['type' => 'string'], + TypeIdentifier::CALLABLE => ['type' => 'string'], + default => ['type' => 'null'], + }; + + return $this->applyNullability($schema, $isNullable); + } + + return ['type' => Schema::UNKNOWN_TYPE]; + } + + /** + * Gets the JSON Schema definition for a class. + */ + private function getClassSchemaDefinition(?string $className, ?bool $readableLink): array + { + if (null === $className) { + return ['type' => 'string']; + } + + if (is_a($className, \DateTimeInterface::class, true)) { + return ['type' => 'string', 'format' => 'date-time']; + } + + if (is_a($className, \DateInterval::class, true)) { + return ['type' => 'string', 'format' => 'duration']; + } + + if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) { + return ['type' => 'string', 'format' => 'uuid']; + } + + if (is_a($className, Ulid::class, true)) { + return ['type' => 'string', 'format' => 'ulid']; + } + + if (is_a($className, \SplFileInfo::class, true)) { + return ['type' => 'string', 'format' => 'binary']; + } + + $isResourceClass = $this->isResourceClass($className); + if (!$isResourceClass && is_a($className, \BackedEnum::class, true)) { + $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases()); + $type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer'; + + return ['type' => $type, 'enum' => $enumCases]; + } + + if (false === $readableLink && $isResourceClass) { + return [ + 'type' => 'string', + 'format' => 'iri-reference', + 'example' => 'https://example.com/', + ]; + } + + return ['type' => Schema::UNKNOWN_TYPE]; + } + + private function getLegacyTypeSchema(ApiProperty $propertyMetadata, array $propertySchema, string $resourceClass, string $property, ?bool $link): array + { + $types = $propertyMetadata->getBuiltinTypes() ?? []; + $className = ($types[0] ?? null)?->getClassName() ?? null; + + if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) && !$className) { + $propertySchema['readOnly'] = true; + } + + if (!\array_key_exists('default', $propertySchema) && !empty($default = $propertyMetadata->getDefault()) && (!$className || !$this->isResourceClass($className))) { + if ($default instanceof \BackedEnum) { + $default = $default->value; + } + $propertySchema['default'] = $default; + } + + if (!\array_key_exists('example', $propertySchema) && !empty($example = $propertyMetadata->getExample())) { + $propertySchema['example'] = $example; } // never override the following keys if at least one is already set or if there's a custom openapi context - if ([] === $types + if ( + [] === $types || ($propertySchema['type'] ?? $propertySchema['$ref'] ?? $propertySchema['anyOf'] ?? $propertySchema['allOf'] ?? $propertySchema['oneOf'] ?? false) - || ($propertyMetadata->getOpenapiContext() ?? false) + || \array_key_exists('type', $propertyMetadata->getOpenapiContext() ?? []) ) { - return $propertyMetadata->withSchema($propertySchema); + return $propertySchema; + } + + if ($propertyMetadata->getUriTemplate()) { + return $propertySchema + [ + 'type' => 'string', + 'format' => 'iri-reference', + 'example' => 'https://example.com/', + ]; } $valueSchema = []; foreach ($types as $type) { // Temp fix for https://github.com/symfony/symfony/pull/52699 if (ArrayCollection::class === $type->getClassName()) { - $type = new Type($type->getBuiltinType(), $type->isNullable(), $type->getClassName(), true, $type->getCollectionKeyTypes(), $type->getCollectionValueTypes()); + $type = new LegacyType($type->getBuiltinType(), $type->isNullable(), $type->getClassName(), true, $type->getCollectionKeyTypes(), $type->getCollectionValueTypes()); } if ($isCollection = $type->isCollection()) { @@ -137,15 +402,14 @@ public function create(string $resourceClass, string $property, array $options = $isCollection = false; } - $propertyType = $this->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $link); + $propertyType = $this->getLegacyType(new LegacyType($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $link); if (!\in_array($propertyType, $valueSchema, true)) { $valueSchema[] = $propertyType; } } - // only one builtInType detected (should be "type" or "$ref") if (1 === \count($valueSchema)) { - return $propertyMetadata->withSchema($propertySchema + $valueSchema[0]); + return $propertySchema + $valueSchema[0]; } // multiple builtInTypes detected: determine oneOf/allOf if union vs intersect types @@ -158,38 +422,38 @@ public function create(string $resourceClass, string $property, array $options = $composition = 'anyOf'; } - return $propertyMetadata->withSchema($propertySchema + [$composition => $valueSchema]); + return $propertySchema + [$composition => $valueSchema]; } - private function getType(Type $type, ?bool $readableLink = null): array + private function getLegacyType(LegacyType $type, ?bool $readableLink = null): array { if (!$type->isCollection()) { - return $this->addNullabilityToTypeDefinition($this->typeToArray($type, $readableLink), $type); + return $this->addNullabilityToTypeDefinition($this->legacyTypeToArray($type, $readableLink), $type); } $keyType = $type->getCollectionKeyTypes()[0] ?? null; - $subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new Type($type->getBuiltinType(), false, $type->getClassName(), false); + $subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new LegacyType($type->getBuiltinType(), false, $type->getClassName(), false); - if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) { + if (null !== $keyType && LegacyType::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) { return $this->addNullabilityToTypeDefinition([ 'type' => 'object', - 'additionalProperties' => $this->getType($subType, $readableLink), + 'additionalProperties' => $this->getLegacyType($subType, $readableLink), ], $type); } return $this->addNullabilityToTypeDefinition([ 'type' => 'array', - 'items' => $this->getType($subType, $readableLink), + 'items' => $this->getLegacyType($subType, $readableLink), ], $type); } - private function typeToArray(Type $type, ?bool $readableLink = null): array + private function legacyTypeToArray(LegacyType $type, ?bool $readableLink = null): array { return match ($type->getBuiltinType()) { - Type::BUILTIN_TYPE_INT => ['type' => 'integer'], - Type::BUILTIN_TYPE_FLOAT => ['type' => 'number'], - Type::BUILTIN_TYPE_BOOL => ['type' => 'boolean'], - Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable(), $readableLink), + LegacyType::BUILTIN_TYPE_INT => ['type' => 'integer'], + LegacyType::BUILTIN_TYPE_FLOAT => ['type' => 'number'], + LegacyType::BUILTIN_TYPE_BOOL => ['type' => 'boolean'], + LegacyType::BUILTIN_TYPE_OBJECT => $this->getLegacyClassType($type->getClassName(), $type->isNullable(), $readableLink), default => ['type' => 'string'], }; } @@ -198,8 +462,12 @@ private function typeToArray(Type $type, ?bool $readableLink = null): array * Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided. * * Note: if the class is not part of exceptions listed above, any class is considered as a resource. + * + * @throws PropertyNotFoundException + * + * @return array */ - private function getClassType(?string $className, bool $nullable, ?bool $readableLink): array + private function getLegacyClassType(?string $className, bool $nullable, ?bool $readableLink): array { if (null === $className) { return ['type' => 'string']; @@ -240,7 +508,8 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl ]; } - if (!$this->isResourceClass($className) && is_a($className, \BackedEnum::class, true)) { + $isResourceClass = $this->isResourceClass($className); + if (!$isResourceClass && is_a($className, \BackedEnum::class, true)) { $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases()); $type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer'; @@ -255,7 +524,7 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl ]; } - if (true !== $readableLink && $this->isResourceClass($className)) { + if (false === $readableLink && $isResourceClass) { return [ 'type' => 'string', 'format' => 'iri-reference', @@ -263,7 +532,8 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl ]; } - // TODO: add propertyNameCollectionFactory and recurse to find the underlying schema? Right now SchemaFactory does the job so we don't compute anything here. + // When this is set, we compute the schema at SchemaFactory::buildPropertySchema as it + // will end up being a $ref to another class schema, we don't have enough informations here return ['type' => Schema::UNKNOWN_TYPE]; } @@ -272,14 +542,14 @@ private function getClassType(?string $className, bool $nullable, ?bool $readabl * * @return array */ - private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type): array + private function addNullabilityToTypeDefinition(array $jsonSchema, LegacyType $type): array { if (!$type->isNullable()) { return $jsonSchema; } if (\array_key_exists('$ref', $jsonSchema)) { - return ['anyOf' => [$jsonSchema, 'type' => 'null']]; + return ['anyOf' => [$jsonSchema, ['type' => 'null']]]; } return [...$jsonSchema, ...[ @@ -288,4 +558,13 @@ private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type): : [$jsonSchema['type'], 'null'], ]]; } + + private function getSchemaValue(array $schema, string $key): array|string|null + { + if (isset($schema['items'])) { + $schema = $schema['items']; + } + + return $schema[$key] ?? $schema['allOf'][0][$key] ?? $schema['anyOf'][0][$key] ?? $schema['oneOf'][0][$key] ?? null; + } } diff --git a/src/JsonSchema/README.md b/src/JsonSchema/README.md index 45d4d62fb65..7ddde68af18 100644 --- a/src/JsonSchema/README.md +++ b/src/JsonSchema/README.md @@ -1,7 +1,14 @@ # API Platform - JSON Schema -Build a JSON Schema from API Resources. +The [JSON Schema](https://json-schema.org/) component of the [API Platform](https://api-platform.com) framework. -## Resources +Generates JSON Schema from PHP classes. +[Documentation](https://api-platform.com/docs/core/json-schema/) +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/JsonSchema/ResourceMetadataTrait.php b/src/JsonSchema/ResourceMetadataTrait.php index 51232c33a34..71988820c76 100644 --- a/src/JsonSchema/ResourceMetadataTrait.php +++ b/src/JsonSchema/ResourceMetadataTrait.php @@ -36,7 +36,7 @@ private function findOutputClass(string $className, string $type, Operation $ope return $forceSubschema ? ($inputOrOutput['class'] ?? $inputOrOutput->class ?? $operation->getClass()) : ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null); } - private function findOperation(string $className, string $type, ?Operation $operation, ?array $serializerContext): Operation + private function findOperation(string $className, string $type, ?Operation $operation, ?array $serializerContext, ?string $format = null): Operation { if (null === $operation) { if (null === $this->resourceMetadataFactory) { @@ -54,7 +54,7 @@ private function findOperation(string $className, string $type, ?Operation $oper $operation = new HttpOperation(); } - return $this->findOperationForType($resourceMetadataCollection, $type, $operation); + return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $forceSubschema ? null : $format); } // The best here is to use an Operation when calling `buildSchema`, we try to do a smart guess otherwise @@ -65,23 +65,28 @@ private function findOperation(string $className, string $type, ?Operation $oper return $resourceMetadataCollection->getOperation($operation->getName()); } - return $this->findOperationForType($resourceMetadataCollection, $type, $operation); + return $this->findOperationForType($resourceMetadataCollection, $type, $operation, $format); } return $operation; } - private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation): Operation + private function findOperationForType(ResourceMetadataCollection $resourceMetadataCollection, string $type, Operation $operation, ?string $format = null): Operation { + $lookForCollection = $operation instanceof CollectionOperationInterface; // Find the operation and use the first one that matches criterias foreach ($resourceMetadataCollection as $resourceMetadata) { foreach ($resourceMetadata->getOperations() ?? [] as $op) { - if ($operation instanceof CollectionOperationInterface && $op instanceof CollectionOperationInterface) { + if (!$lookForCollection && $op instanceof CollectionOperationInterface) { + continue; + } + + if (Schema::TYPE_INPUT === $type && \in_array($op->getMethod(), ['POST', 'PATCH', 'PUT'], true)) { $operation = $op; break 2; } - if (Schema::TYPE_INPUT === $type && \in_array($op->getMethod(), ['POST', 'PATCH', 'PUT'], true)) { + if ($format && Schema::TYPE_OUTPUT === $type && \array_key_exists($format, $op->getOutputFormats() ?? [])) { $operation = $op; break 2; } diff --git a/src/JsonSchema/Schema.php b/src/JsonSchema/Schema.php index 9d7aa40a966..a6ea1d43731 100644 --- a/src/JsonSchema/Schema.php +++ b/src/JsonSchema/Schema.php @@ -123,7 +123,10 @@ private function removeDefinitionKeyPrefix(string $definitionKey): string { // strlen('#/definitions/') = 14 // strlen('#/components/schemas/') = 21 - $prefix = self::VERSION_OPENAPI === $this->version ? 21 : 14; + $prefix = match ($this->version) { + self::VERSION_OPENAPI => 21, + default => 14, + }; return substr($definitionKey, $prefix); } diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 097eaddd4b8..bfbf57b4d16 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -22,8 +22,16 @@ use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\TypeHelper; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\TypeInfo\Type\BuiltinType; +use Symfony\Component\TypeInfo\Type\CollectionType; +use Symfony\Component\TypeInfo\Type\CompositeTypeInterface; +use Symfony\Component\TypeInfo\Type\GenericType; +use Symfony\Component\TypeInfo\Type\ObjectType; +use Symfony\Component\TypeInfo\TypeIdentifier; /** * {@inheritdoc} @@ -33,19 +41,18 @@ final class SchemaFactory implements SchemaFactoryInterface, SchemaFactoryAwareInterface { use ResourceMetadataTrait; - private ?TypeFactoryInterface $typeFactory = null; + use SchemaUriPrefixTrait; + + private const JSON_MERGE_PATCH_SCHEMA_POSTFIX = '.jsonMergePatch'; + private ?SchemaFactoryInterface $schemaFactory = null; // Edge case where the related resource is not readable (for example: NotExposed) but we have groups to read the whole related object - public const FORCE_SUBSCHEMA = '_api_subschema_force_readable_link'; public const OPENAPI_DEFINITION_NAME = 'openapi_definition_name'; - public function __construct(?TypeFactoryInterface $typeFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, private readonly ?array $distinctFormats = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null) + public function __construct(ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory, private readonly ?NameConverterInterface $nameConverter = null, ?ResourceClassResolverInterface $resourceClassResolver = null, ?array $distinctFormats = null, private ?DefinitionNameFactoryInterface $definitionNameFactory = null) { - if ($typeFactory) { - $this->typeFactory = $typeFactory; - } if (!$definitionNameFactory) { - $this->definitionNameFactory = new DefinitionNameFactory($this->distinctFormats); + $this->definitionNameFactory = new DefinitionNameFactory($distinctFormats); } $this->resourceMetadataFactory = $resourceMetadataFactory; @@ -64,7 +71,7 @@ public function buildSchema(string $className, string $format = 'json', string $ $inputOrOutputClass = $className; $serializerContext ??= []; } else { - $operation = $this->findOperation($className, $type, $operation, $serializerContext); + $operation = $this->findOperation($className, $type, $operation, $serializerContext, $format); $inputOrOutputClass = $this->findOutputClass($className, $type, $operation, $serializerContext); $serializerContext ??= $this->getSerializerContext($operation, $type); } @@ -77,7 +84,6 @@ public function buildSchema(string $className, string $format = 'json', string $ $validationGroups = $operation ? $this->getValidationGroups($operation) : []; $version = $schema->getVersion(); $definitionName = $this->definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext); - $method = $operation instanceof HttpOperation ? $operation->getMethod() : 'GET'; if (!$operation) { $method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET'; @@ -88,8 +94,13 @@ public function buildSchema(string $className, string $format = 'json', string $ return $schema; } + $isJsonMergePatch = 'json' === $format && 'PATCH' === $method && Schema::TYPE_INPUT === $type; + if ($isJsonMergePatch) { + $definitionName .= self::JSON_MERGE_PATCH_SCHEMA_POSTFIX; + } + if (!isset($schema['$ref']) && !isset($schema['type'])) { - $ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName; + $ref = $this->getSchemaUriPrefix($version).$definitionName; if ($forceCollection || ('POST' !== $method && $operation instanceof CollectionOperationInterface)) { $schema['type'] = 'array'; $schema['items'] = ['$ref' => $ref]; @@ -107,7 +118,9 @@ public function buildSchema(string $className, string $format = 'json', string $ /** @var \ArrayObject $definition */ $definition = new \ArrayObject(['type' => 'object']); $definitions[$definitionName] = $definition; - $definition['description'] = $operation ? ($operation->getDescription() ?? '') : ''; + if ($description = $operation?->getDescription()) { + $definition['description'] = $description; + } // additionalProperties are allowed by default, so it does not need to be set explicitly, unless allow_extra_attributes is false // See https://json-schema.org/understanding-json-schema/reference/object.html#properties @@ -118,8 +131,6 @@ public function buildSchema(string $className, string $format = 'json', string $ // see https://github.com/json-schema-org/json-schema-spec/pull/737 if (Schema::VERSION_SWAGGER !== $version && $operation && $operation->getDeprecationReason()) { $definition['deprecated'] = true; - } else { - $definition['deprecated'] = false; } // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it @@ -131,22 +142,30 @@ public function buildSchema(string $className, string $format = 'json', string $ $options = ['schema_type' => $type] + $this->getFactoryOptions($serializerContext, $validationGroups, $operation instanceof HttpOperation ? $operation : null); foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) { $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName, $options); - if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) { + + if (false === $propertyMetadata->isReadable() && false === $propertyMetadata->isWritable()) { continue; } $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName; - if ($propertyMetadata->isRequired()) { + if ($propertyMetadata->isRequired() && !$isJsonMergePatch) { $definition['required'][] = $normalizedPropertyName; } - $this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format, $type); + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $this->buildLegacyPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format, $type); + } else { + $this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format, $type); + } } return $schema; } - private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void + /** + * Builds the JSON Schema for a property using the legacy PropertyInfo component. + */ + private function buildLegacyPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void { $version = $schema->getVersion(); if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) { @@ -165,7 +184,7 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str unset($propertySchema['type']); } - $extraProperties = $propertyMetadata->getExtraProperties() ?? []; + $extraProperties = $propertyMetadata->getExtraProperties(); // see AttributePropertyMetadataFactory if (true === ($extraProperties[SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED] ?? false)) { // schema seems to have been declared by the user: do not override nor complete user value @@ -182,8 +201,10 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $propertySchemaType = $propertySchema['type'] ?? false; $isUnknown = Schema::UNKNOWN_TYPE === $propertySchemaType - || ('array' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['items']['type'] ?? null)); + || ('array' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['items']['type'] ?? null)) + || ('object' === $propertySchemaType && Schema::UNKNOWN_TYPE === ($propertySchema['additionalProperties']['type'] ?? null)); + // Scalar properties if ( !$isUnknown && ( [] === $types @@ -192,6 +213,10 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str || ($propertySchema['format'] ?? $propertySchema['enum'] ?? false) ) ) { + if (isset($propertySchema['$ref'])) { + unset($propertySchema['type']); + } + $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema); return; @@ -207,12 +232,6 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $subSchema = new Schema($version); $subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema - // TODO: in 3.3 add trigger_deprecation() as type factories are not used anymore, we moved this logic to SchemaPropertyMetadataFactory so that it gets cached - if ($typeFromFactory = $this->typeFactory?->getType($type, 'jsonschema', $propertyMetadata->isReadableLink(), $serializerContext)) { - $propertySchema = $typeFromFactory; - break; - } - $isCollection = $type->isCollection(); if ($isCollection) { $valueType = $type->getCollectionValueTypes()[0] ?? null; @@ -226,14 +245,24 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str } $subSchemaFactory = $this->schemaFactory ?: $this; - $subSchema = $subSchemaFactory->buildSchema($className, $format, $parentType, null, $subSchema, $serializerContext + [self::FORCE_SUBSCHEMA => true], false); + $subSchema = $subSchemaFactory->buildSchema( + $className, + $format, + $parentType, + null, + $subSchema, + $serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true], + false, + ); + if (!isset($subSchema['$ref'])) { continue; } if ($isCollection) { - $propertySchema['items']['$ref'] = $subSchema['$ref']; - unset($propertySchema['items']['type']); + $key = ($propertySchema['type'] ?? null) === 'object' ? 'additionalProperties' : 'items'; + $propertySchema[$key]['$ref'] = $subSchema['$ref']; + unset($propertySchema[$key]['type']); break; } @@ -245,7 +274,8 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $refs[] = ['type' => 'null']; } - if (($c = \count($refs)) > 1) { + $c = \count($refs); + if ($c > 1) { $propertySchema['anyOf'] = $refs; unset($propertySchema['type']); } elseif (1 === $c) { @@ -256,6 +286,138 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema); } + private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, ApiProperty $propertyMetadata, array $serializerContext, string $format, string $parentType): void + { + $version = $schema->getVersion(); + if (Schema::VERSION_SWAGGER === $version || Schema::VERSION_OPENAPI === $version) { + $additionalPropertySchema = $propertyMetadata->getOpenapiContext(); + } else { + $additionalPropertySchema = $propertyMetadata->getJsonSchemaContext(); + } + + $propertySchema = array_merge( + $propertyMetadata->getSchema() ?? [], + $additionalPropertySchema ?? [] + ); + + $extraProperties = $propertyMetadata->getExtraProperties(); + // see AttributePropertyMetadataFactory + if (true === ($extraProperties[SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED] ?? false)) { + // schema seems to have been declared by the user: do not override nor complete user value + $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema); + + return; + } + + $type = $propertyMetadata->getNativeType(); + + // Type is defined in an allOf, anyOf, or oneOf + $propertySchemaType = $this->getSchemaValue($propertySchema, 'type'); + $currentRef = $this->getSchemaValue($propertySchema, '$ref'); + $isSchemaDefined = null !== ($currentRef ?? $this->getSchemaValue($propertySchema, 'format') ?? $this->getSchemaValue($propertySchema, 'enum')); + if (!$isSchemaDefined && Schema::UNKNOWN_TYPE !== $propertySchemaType) { + $isSchemaDefined = true; + } + + // Check if the type is considered "unknown" by SchemaPropertyMetadataFactory + if (isset($propertySchema['additionalProperties']['type']) && Schema::UNKNOWN_TYPE === $propertySchema['additionalProperties']['type']) { + $isSchemaDefined = false; + } + + if ($isSchemaDefined && Schema::UNKNOWN_TYPE !== $propertySchemaType) { + // If schema is defined and not marked as unknown, or if no type info exists, return early + $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema); + + return; + } + + if (Schema::UNKNOWN_TYPE === $propertySchemaType) { + $propertySchema = []; + } + + // property schema is created in SchemaPropertyMetadataFactory, but it cannot build resource reference ($ref) + // complete property schema with resource reference ($ref) if it's related to an object/resource + $refs = []; + $isNullable = $type?->isNullable() ?? false; + + // TODO: refactor this with TypeInfo we shouldn't have to loop like this, the below code handles object refs + if ($type) { + foreach ($type instanceof CompositeTypeInterface ? $type->getTypes() : [$type] as $t) { + if ($t instanceof BuiltinType && TypeIdentifier::NULL === $t->getTypeIdentifier()) { + continue; + } + + $valueType = $t; + $isCollection = $t instanceof CollectionType; + + if ($isCollection) { + $valueType = TypeHelper::getCollectionValueType($t); + } + + if (!$valueType instanceof ObjectType && !$valueType instanceof GenericType) { + continue; + } + + if ($valueType instanceof ObjectType) { + $className = $valueType->getClassName(); + } else { + // GenericType + $className = $valueType->getWrappedType()->getClassName(); + } + + $subSchemaInstance = new Schema($version); + $subSchemaInstance->setDefinitions($schema->getDefinitions()); + $subSchemaFactory = $this->schemaFactory ?: $this; + $subSchemaResult = $subSchemaFactory->buildSchema( + $className, + $format, + $parentType, + null, + $subSchemaInstance, + $serializerContext + [self::FORCE_SUBSCHEMA => true, 'gen_id' => $propertyMetadata->getGenId() ?? true], + false, + ); + if (!isset($subSchemaResult['$ref'])) { + continue; + } + + if ($isCollection) { + $key = ($propertySchema['type'] ?? null) === 'object' ? 'additionalProperties' : 'items'; + if (!isset($propertySchema['type'])) { + $propertySchema['type'] = 'array'; + } + + if (!isset($propertySchema[$key]) || !\is_array($propertySchema[$key])) { + $propertySchema[$key] = []; + } + $propertySchema[$key] = ['$ref' => $subSchemaResult['$ref']]; + $refs = []; + break; + } + + $refs[] = ['$ref' => $subSchemaResult['$ref']]; + } + } + + if (!empty($refs)) { + if ($isNullable) { + $refs[] = ['type' => 'null']; + } + + if (($c = \count($refs)) > 1) { + $propertySchema = ['anyOf' => $refs]; + } elseif (1 === $c) { + $propertySchema = ['$ref' => $refs[0]['$ref']]; + } + } + + if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) && !isset($propertySchema['$ref'])) { + $propertySchema['readOnly'] = true; + } + + $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = new \ArrayObject($propertySchema); + } + private function getValidationGroups(Operation $operation): array { $groups = $operation->getValidationContext()['groups'] ?? []; @@ -290,6 +452,10 @@ private function getFactoryOptions(array $serializerContext, array $validationGr $options['validation_groups'] = $validationGroups; } + if ($operation && ($ignoredAttributes = $operation->getNormalizationContext()['ignored_attributes'] ?? null)) { + $options['ignored_attributes'] = $ignoredAttributes; + } + return $options; } @@ -297,4 +463,13 @@ public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void { $this->schemaFactory = $schemaFactory; } + + private function getSchemaValue(array $schema, string $key): array|string|null + { + if (isset($schema['items'])) { + $schema = $schema['items']; + } + + return $schema[$key] ?? $schema['allOf'][0][$key] ?? $schema['anyOf'][0][$key] ?? $schema['oneOf'][0][$key] ?? null; + } } diff --git a/src/JsonSchema/SchemaFactoryInterface.php b/src/JsonSchema/SchemaFactoryInterface.php index ec992908a50..64b270764a6 100644 --- a/src/JsonSchema/SchemaFactoryInterface.php +++ b/src/JsonSchema/SchemaFactoryInterface.php @@ -22,6 +22,8 @@ */ interface SchemaFactoryInterface { + public const FORCE_SUBSCHEMA = '_api_subschema_force_readable_link'; + /** * Builds the JSON Schema document corresponding to the given PHP class. */ diff --git a/src/JsonSchema/SchemaUriPrefixTrait.php b/src/JsonSchema/SchemaUriPrefixTrait.php new file mode 100644 index 00000000000..de5efd96c10 --- /dev/null +++ b/src/JsonSchema/SchemaUriPrefixTrait.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema; + +/** + * @internal + */ +trait SchemaUriPrefixTrait +{ + public function getSchemaUriPrefix(string $version): string + { + return match ($version) { + Schema::VERSION_OPENAPI => '#/components/schemas/', + default => '#/definitions/', + }; + } +} diff --git a/tests/JsonSchema/DefinitionNameFactoryTest.php b/src/JsonSchema/Tests/DefinitionNameFactoryTest.php similarity index 84% rename from tests/JsonSchema/DefinitionNameFactoryTest.php rename to src/JsonSchema/Tests/DefinitionNameFactoryTest.php index 519fcfcad42..e50764ea76e 100644 --- a/tests/JsonSchema/DefinitionNameFactoryTest.php +++ b/src/JsonSchema/Tests/DefinitionNameFactoryTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace ApiPlatform\Tests\JsonSchema; +namespace ApiPlatform\JsonSchema\Tests; use ApiPlatform\JsonSchema\DefinitionNameFactory; use ApiPlatform\JsonSchema\SchemaFactory; @@ -62,36 +62,36 @@ public static function providerDefinitions(): iterable yield ['Bar.DtoOutput.jsonld-read_write', Dummy::class, 'jsonld', DtoOutput::class, new Get(shortName: 'Bar'), [AbstractNormalizer::GROUPS => ['read', 'write']]]; } - /** @dataProvider providerDefinitions */ + #[\PHPUnit\Framework\Attributes\DataProvider('providerDefinitions')] public function testCreate(string $expected, string $className, string $format = 'json', ?string $inputOrOutputClass = null, ?Operation $operation = null, array $serializerContext = []): void { - $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + $definitionNameFactory = new DefinitionNameFactory(); static::assertSame($expected, $definitionNameFactory->create($className, $format, $inputOrOutputClass, $operation, $serializerContext)); } public function testCreateDifferentPrefixesForClassesWithTheSameShortName(): void { - $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true]); + $definitionNameFactory = new DefinitionNameFactory(); self::assertEquals( 'DummyClass.jsonapi', - $definitionNameFactory->create(\ApiPlatform\Tests\JsonSchema\Dummy\NamespaceA\Module\DummyClass::class, 'jsonapi') + $definitionNameFactory->create(Fixtures\DefinitionNameFactory\NamespaceA\Module\DummyClass::class, 'jsonapi') ); self::assertEquals( 'Module.DummyClass.jsonapi', - $definitionNameFactory->create(\ApiPlatform\Tests\JsonSchema\Dummy\NamespaceB\Module\DummyClass::class, 'jsonapi') + $definitionNameFactory->create(Fixtures\DefinitionNameFactory\NamespaceB\Module\DummyClass::class, 'jsonapi') ); self::assertEquals( 'NamespaceC.Module.DummyClass.jsonapi', - $definitionNameFactory->create(\ApiPlatform\Tests\JsonSchema\Dummy\NamespaceC\Module\DummyClass::class, 'jsonapi') + $definitionNameFactory->create(Fixtures\DefinitionNameFactory\NamespaceC\Module\DummyClass::class, 'jsonapi') ); self::assertEquals( 'DummyClass.jsonhal', - $definitionNameFactory->create(\ApiPlatform\Tests\JsonSchema\Dummy\NamespaceA\Module\DummyClass::class, 'jsonhal') + $definitionNameFactory->create(Fixtures\DefinitionNameFactory\NamespaceA\Module\DummyClass::class, 'jsonhal') ); } } diff --git a/src/JsonSchema/Tests/Fixtures/DefinitionNameFactory/NamespaceA/Module/DummyClass.php b/src/JsonSchema/Tests/Fixtures/DefinitionNameFactory/NamespaceA/Module/DummyClass.php new file mode 100644 index 00000000000..6663f66b941 --- /dev/null +++ b/src/JsonSchema/Tests/Fixtures/DefinitionNameFactory/NamespaceA/Module/DummyClass.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Tests\Fixtures\DefinitionNameFactory\NamespaceA\Module; + +class DummyClass +{ +} diff --git a/src/JsonSchema/Tests/Fixtures/DefinitionNameFactory/NamespaceB/Module/DummyClass.php b/src/JsonSchema/Tests/Fixtures/DefinitionNameFactory/NamespaceB/Module/DummyClass.php new file mode 100644 index 00000000000..5b043d1ddc3 --- /dev/null +++ b/src/JsonSchema/Tests/Fixtures/DefinitionNameFactory/NamespaceB/Module/DummyClass.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Tests\Fixtures\DefinitionNameFactory\NamespaceB\Module; + +class DummyClass +{ +} diff --git a/src/JsonSchema/Tests/Fixtures/DefinitionNameFactory/NamespaceC/Module/DummyClass.php b/src/JsonSchema/Tests/Fixtures/DefinitionNameFactory/NamespaceC/Module/DummyClass.php new file mode 100644 index 00000000000..1790061a52c --- /dev/null +++ b/src/JsonSchema/Tests/Fixtures/DefinitionNameFactory/NamespaceC/Module/DummyClass.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Tests\Fixtures\DefinitionNameFactory\NamespaceC\Module; + +class DummyClass +{ +} diff --git a/src/JsonSchema/Tests/Fixtures/DummyWithCustomOpenApiContext.php b/src/JsonSchema/Tests/Fixtures/DummyWithCustomOpenApiContext.php index 22517c6cdf5..c676ea81eb9 100644 --- a/src/JsonSchema/Tests/Fixtures/DummyWithCustomOpenApiContext.php +++ b/src/JsonSchema/Tests/Fixtures/DummyWithCustomOpenApiContext.php @@ -30,4 +30,10 @@ class DummyWithCustomOpenApiContext { #[ApiProperty(openapiContext: ['type' => 'object', 'properties' => ['alpha' => ['type' => 'integer']]])] public $acme; + + #[ApiProperty(openapiContext: ['description' => 'My description'])] + public bool $foo; + + #[ApiProperty(openapiContext: ['iris' => 'https://schema.org/Date'])] + public \DateTimeImmutable $bar; } diff --git a/src/JsonSchema/Tests/Fixtures/DummyWithUnionTypeProperty.php b/src/JsonSchema/Tests/Fixtures/DummyWithUnionTypeProperty.php new file mode 100644 index 00000000000..4d440d9c993 --- /dev/null +++ b/src/JsonSchema/Tests/Fixtures/DummyWithUnionTypeProperty.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Tests\Fixtures; + +use ApiPlatform\Metadata\ApiResource; + +#[ApiResource] +class DummyWithUnionTypeProperty +{ + public string|int $unionProperty; +} diff --git a/src/JsonSchema/Tests/Fixtures/GenericChild.php b/src/JsonSchema/Tests/Fixtures/GenericChild.php new file mode 100644 index 00000000000..47d017972f1 --- /dev/null +++ b/src/JsonSchema/Tests/Fixtures/GenericChild.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\JsonSchema\Tests\Fixtures; + +/** + * @template T of object + */ +class GenericChild +{ + public string $property; +} diff --git a/src/JsonSchema/Tests/Fixtures/NotAResource.php b/src/JsonSchema/Tests/Fixtures/NotAResource.php index f6a5519c68b..0ef16478cbd 100644 --- a/src/JsonSchema/Tests/Fixtures/NotAResource.php +++ b/src/JsonSchema/Tests/Fixtures/NotAResource.php @@ -13,7 +13,7 @@ namespace ApiPlatform\JsonSchema\Tests\Fixtures; -use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Attribute\Groups; /** * This class is not mapped as an API resource. @@ -22,15 +22,15 @@ */ class NotAResource { + /** + * @param array> $items + */ public function __construct( - /** - * @Groups("contain_non_resource") - */ + #[Groups('contain_non_resource')] private $foo, - /** - * @Groups("contain_non_resource") - */ + #[Groups('contain_non_resource')] private $bar, + private array $items, ) { } @@ -43,4 +43,12 @@ public function getBar() { return $this->bar; } + + /** + * @return array> + */ + public function getItems() + { + return $this->items; + } } diff --git a/src/JsonSchema/Tests/Metadata/Property/Factory/SchemaPropertyMetadataFactoryTest.php b/src/JsonSchema/Tests/Metadata/Property/Factory/SchemaPropertyMetadataFactoryTest.php index ec0817014db..ce930485a04 100644 --- a/src/JsonSchema/Tests/Metadata/Property/Factory/SchemaPropertyMetadataFactoryTest.php +++ b/src/JsonSchema/Tests/Metadata/Property/Factory/SchemaPropertyMetadataFactoryTest.php @@ -16,19 +16,35 @@ use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory; use ApiPlatform\JsonSchema\Tests\Fixtures\DummyWithCustomOpenApiContext; use ApiPlatform\JsonSchema\Tests\Fixtures\DummyWithEnum; +use ApiPlatform\JsonSchema\Tests\Fixtures\DummyWithUnionTypeProperty; use ApiPlatform\JsonSchema\Tests\Fixtures\Enum\IntEnumAsIdentifier; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; +use Symfony\Component\TypeInfo\Type; class SchemaPropertyMetadataFactoryTest extends TestCase { + #[IgnoreDeprecations] + public function testEnumLegacy(): void + { + $this->expectUserDeprecationMessage('Since api_platform/metadata 4.2: The "builtinTypes" argument of "ApiPlatform\Metadata\ApiProperty" is deprecated, use "nativeType" instead.'); + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $apiProperty = new ApiProperty(builtinTypes: [new LegacyType(builtinType: 'object', nullable: true, class: IntEnumAsIdentifier::class)]); + $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); + $decorated->expects($this->once())->method('create')->with(DummyWithEnum::class, 'intEnumAsIdentifier')->willReturn($apiProperty); + $schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated); + $apiProperty = $schemaPropertyMetadataFactory->create(DummyWithEnum::class, 'intEnumAsIdentifier'); + $this->assertEquals(['type' => ['integer', 'null'], 'enum' => [1, 2, null]], $apiProperty->getSchema()); + } + public function testEnum(): void { $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); - $apiProperty = new ApiProperty(builtinTypes: [new Type(builtinType: 'object', nullable: true, class: IntEnumAsIdentifier::class)]); + $apiProperty = new ApiProperty(nativeType: Type::nullable(Type::enum(IntEnumAsIdentifier::class))); $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); $decorated->expects($this->once())->method('create')->with(DummyWithEnum::class, 'intEnumAsIdentifier')->willReturn($apiProperty); $schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated); @@ -36,11 +52,27 @@ public function testEnum(): void $this->assertEquals(['type' => ['integer', 'null'], 'enum' => [1, 2, null]], $apiProperty->getSchema()); } + #[IgnoreDeprecations] + public function testWithCustomOpenApiContextLegacy(): void + { + $this->expectUserDeprecationMessage('Since api_platform/metadata 4.2: The "builtinTypes" argument of "ApiPlatform\Metadata\ApiProperty" is deprecated, use "nativeType" instead.'); + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $apiProperty = new ApiProperty( + builtinTypes: [new LegacyType(builtinType: 'object', nullable: true, class: IntEnumAsIdentifier::class)], + openapiContext: ['type' => 'object', 'properties' => ['alpha' => ['type' => 'integer']]], + ); + $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); + $decorated->expects($this->once())->method('create')->with(DummyWithCustomOpenApiContext::class, 'acme')->willReturn($apiProperty); + $schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated); + $apiProperty = $schemaPropertyMetadataFactory->create(DummyWithCustomOpenApiContext::class, 'acme'); + $this->assertEquals([], $apiProperty->getSchema()); + } + public function testWithCustomOpenApiContext(): void { $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); $apiProperty = new ApiProperty( - builtinTypes: [new Type(builtinType: 'object', nullable: true, class: IntEnumAsIdentifier::class)], + nativeType: Type::nullable(Type::enum(IntEnumAsIdentifier::class)), openapiContext: ['type' => 'object', 'properties' => ['alpha' => ['type' => 'integer']]], ); $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); @@ -49,4 +81,90 @@ public function testWithCustomOpenApiContext(): void $apiProperty = $schemaPropertyMetadataFactory->create(DummyWithCustomOpenApiContext::class, 'acme'); $this->assertEquals([], $apiProperty->getSchema()); } + + #[IgnoreDeprecations] + public function testWithCustomOpenApiContextWithoutTypeDefinitionLegacy(): void + { + $this->expectUserDeprecationMessage('Since api_platform/metadata 4.2: The "builtinTypes" argument of "ApiPlatform\Metadata\ApiProperty" is deprecated, use "nativeType" instead.'); + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $apiProperty = new ApiProperty( + openapiContext: ['description' => 'My description'], + builtinTypes: [new LegacyType(builtinType: 'bool')], + ); + $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); + $decorated->expects($this->once())->method('create')->with(DummyWithCustomOpenApiContext::class, 'foo')->willReturn($apiProperty); + $schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated); + $apiProperty = $schemaPropertyMetadataFactory->create(DummyWithCustomOpenApiContext::class, 'foo'); + $this->assertEquals([ + 'type' => 'boolean', + ], $apiProperty->getSchema()); + + $apiProperty = new ApiProperty( + openapiContext: ['iris' => 'https://schema.org/Date'], + builtinTypes: [new LegacyType(builtinType: 'object', class: \DateTimeImmutable::class)], + ); + $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); + $decorated->expects($this->once())->method('create')->with(DummyWithCustomOpenApiContext::class, 'bar')->willReturn($apiProperty); + $schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated); + $apiProperty = $schemaPropertyMetadataFactory->create(DummyWithCustomOpenApiContext::class, 'bar'); + $this->assertEquals([ + 'type' => 'string', + 'format' => 'date-time', + ], $apiProperty->getSchema()); + } + + public function testWithCustomOpenApiContextWithoutTypeDefinition(): void + { + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $apiProperty = + new ApiProperty( + openapiContext: ['description' => 'My description'], + nativeType: Type::bool(), + ); + $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); + $decorated->expects($this->once())->method('create')->with(DummyWithCustomOpenApiContext::class, 'foo')->willReturn($apiProperty); + $schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated); + $apiProperty = $schemaPropertyMetadataFactory->create(DummyWithCustomOpenApiContext::class, 'foo'); + $this->assertEquals([ + 'type' => 'boolean', + ], $apiProperty->getSchema()); + + $apiProperty = + new ApiProperty( + openapiContext: ['iris' => 'https://schema.org/Date'], + nativeType: Type::object(\DateTimeImmutable::class), + ); + $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); + $decorated->expects($this->once())->method('create')->with(DummyWithCustomOpenApiContext::class, 'bar')->willReturn($apiProperty); + $schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated); + $apiProperty = $schemaPropertyMetadataFactory->create(DummyWithCustomOpenApiContext::class, 'bar'); + $this->assertEquals([ + 'type' => 'string', + 'format' => 'date-time', + ], $apiProperty->getSchema()); + } + + public function testUnionTypeAnyOfIsArray(): void + { + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { // @phpstan-ignore-line symfony/property-info 6.4 is still allowed and this may be true + $this->markTestSkipped('This test only supports type-info component'); + } + + $resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class); + $apiProperty = new ApiProperty(nativeType: Type::union(Type::string(), Type::int())); + $decorated = $this->createMock(PropertyMetadataFactoryInterface::class); + $decorated->expects($this->once())->method('create')->with(DummyWithUnionTypeProperty::class, 'unionProperty')->willReturn($apiProperty); + + $schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated); + $apiProperty = $schemaPropertyMetadataFactory->create(DummyWithUnionTypeProperty::class, 'unionProperty'); + + $expectedSchema = [ + 'anyOf' => [ + ['type' => 'integer'], + ['type' => 'string'], + ], + ]; + + $this->assertEquals($expectedSchema, $apiProperty->getSchema()); + } } diff --git a/src/JsonSchema/Tests/SchemaFactoryTest.php b/src/JsonSchema/Tests/SchemaFactoryTest.php index bf3bd56ae46..b75107a57ea 100644 --- a/src/JsonSchema/Tests/SchemaFactoryTest.php +++ b/src/JsonSchema/Tests/SchemaFactoryTest.php @@ -19,6 +19,7 @@ use ApiPlatform\JsonSchema\Tests\Fixtures\ApiResource\OverriddenOperationDummy; use ApiPlatform\JsonSchema\Tests\Fixtures\DummyResourceInterface; use ApiPlatform\JsonSchema\Tests\Fixtures\Enum\GenderTypeEnum; +use ApiPlatform\JsonSchema\Tests\Fixtures\GenericChild; use ApiPlatform\JsonSchema\Tests\Fixtures\NotAResource; use ApiPlatform\JsonSchema\Tests\Fixtures\NotAResourceWithUnionIntersectTypes; use ApiPlatform\JsonSchema\Tests\Fixtures\Serializable; @@ -32,18 +33,22 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\ResourceClassResolverInterface; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\PropertyInfo\Type as LegacyType; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\TypeInfo\Type; class SchemaFactoryTest extends TestCase { use ProphecyTrait; - public function testBuildSchemaForNonResourceClass(): void + #[IgnoreDeprecations] + public function testBuildSchemaForNonResourceClassLegacy(): void { + $this->expectUserDeprecationMessage('Since api-platform/metadata 4.2: The "ApiPlatform\Metadata\ApiProperty::withBuiltinTypes()" method is deprecated, use "ApiPlatform\Metadata\ApiProperty::withNativeType()" instead.'); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); @@ -52,13 +57,13 @@ public function testBuildSchemaForNonResourceClass(): void $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(NotAResource::class, 'foo', Argument::cetera())->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]) ->withReadable(true) ->withSchema(['type' => 'string']) ); $propertyMetadataFactoryProphecy->create(NotAResource::class, 'bar', Argument::cetera())->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)]) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_INT)]) ->withReadable(true) ->withDefault('default_bar') ->withExample('example_bar') @@ -66,7 +71,7 @@ public function testBuildSchemaForNonResourceClass(): void ); $propertyMetadataFactoryProphecy->create(NotAResource::class, 'genderType', Argument::cetera())->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT)]) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT)]) ->withReadable(true) ->withDefault('male') ->withSchema(['type' => 'object', 'default' => 'male', 'example' => 'male']) @@ -75,10 +80,9 @@ public function testBuildSchemaForNonResourceClass(): void $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false); - $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + $definitionNameFactory = new DefinitionNameFactory(); $schemaFactory = new SchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), @@ -118,6 +122,179 @@ public function testBuildSchemaForNonResourceClass(): void $this->assertSame('male', $definitions[$rootDefinitionKey]['properties']['genderType']['example']); } + public function testBuildSchemaForNonResourceClass(): void + { + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { // @phpstan-ignore-line symfony/property-info 6.4 is still allowed and this may be true + $this->markTestSkipped('This test only supports type-info component'); + } + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(NotAResource::class, Argument::cetera())->willReturn(new PropertyNameCollection(['foo', 'bar', 'genderType', 'items'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(NotAResource::class, 'foo', Argument::cetera())->willReturn( + (new ApiProperty()) + ->withNativeType(Type::string()) + ->withReadable(true) + ->withSchema(['type' => 'string']) + ); + $propertyMetadataFactoryProphecy->create(NotAResource::class, 'bar', Argument::cetera())->willReturn( + (new ApiProperty()) + ->withNativeType(Type::int()) + ->withReadable(true) + ->withDefault('default_bar') + ->withExample('example_bar') + ->withSchema(['type' => 'integer', 'default' => 'default_bar', 'example' => 'example_bar']) + ); + $propertyMetadataFactoryProphecy->create(NotAResource::class, 'genderType', Argument::cetera())->willReturn( + (new ApiProperty()) + ->withNativeType(Type::object()) + ->withReadable(true) + ->withDefault('male') + ->withSchema(['type' => 'object', 'default' => 'male', 'example' => 'male']) + ); + $propertyMetadataFactoryProphecy->create(NotAResource::class, 'items', Argument::cetera())->willReturn( + (new ApiProperty()) + ->withNativeType( + Type::generic(Type::object(GenericChild::class), Type::int()), + ) + ->withReadable(true) + ->withSchema(['type' => Schema::UNKNOWN_TYPE]) + ); + + $propertyNameCollectionFactoryProphecy->create(GenericChild::class, Argument::cetera()) + ->willReturn(new PropertyNameCollection(['property'])); + $propertyMetadataFactoryProphecy->create(GenericChild::class, 'property', Argument::cetera()) + ->willReturn( + (new ApiProperty()) + ->withNativeType(Type::string()) + ->withReadable(true) + ->withSchema(['type' => 'string']) + ); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false); + $resourceClassResolverProphecy->isResourceClass(GenericChild::class)->willReturn(false); + + $definitionNameFactory = new DefinitionNameFactory(); + + $schemaFactory = new SchemaFactory( + resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), + propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), + resourceClassResolver: $resourceClassResolverProphecy->reveal(), + definitionNameFactory: $definitionNameFactory, + ); + $resultSchema = $schemaFactory->buildSchema(NotAResource::class); + + $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); + $definitions = $resultSchema->getDefinitions(); + + $this->assertSame((new \ReflectionClass(NotAResource::class))->getShortName(), $rootDefinitionKey); + $this->assertTrue(isset($definitions[$rootDefinitionKey])); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]); + $this->assertSame('object', $definitions[$rootDefinitionKey]['type']); + $this->assertArrayNotHasKey('additionalProperties', $definitions[$rootDefinitionKey]); + $this->assertArrayHasKey('properties', $definitions[$rootDefinitionKey]); + $this->assertArrayHasKey('foo', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['foo']); + $this->assertArrayNotHasKey('default', $definitions[$rootDefinitionKey]['properties']['foo']); + $this->assertArrayNotHasKey('example', $definitions[$rootDefinitionKey]['properties']['foo']); + $this->assertSame('string', $definitions[$rootDefinitionKey]['properties']['foo']['type']); + $this->assertArrayHasKey('bar', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['bar']); + $this->assertArrayHasKey('default', $definitions[$rootDefinitionKey]['properties']['bar']); + $this->assertArrayHasKey('example', $definitions[$rootDefinitionKey]['properties']['bar']); + $this->assertSame('integer', $definitions[$rootDefinitionKey]['properties']['bar']['type']); + $this->assertSame('default_bar', $definitions[$rootDefinitionKey]['properties']['bar']['default']); + $this->assertSame('example_bar', $definitions[$rootDefinitionKey]['properties']['bar']['example']); + + $this->assertArrayHasKey('genderType', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayHasKey('default', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayHasKey('example', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['genderType']['type']); + $this->assertSame('male', $definitions[$rootDefinitionKey]['properties']['genderType']['default']); + $this->assertSame('male', $definitions[$rootDefinitionKey]['properties']['genderType']['example']); + + $this->assertArrayHasKey('items', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('$ref', $definitions[$rootDefinitionKey]['properties']['items']); + $this->assertSame('#/definitions/GenericChild', $definitions[$rootDefinitionKey]['properties']['items']['$ref']); + } + + #[IgnoreDeprecations] + public function testBuildSchemaForNonResourceClassWithUnionIntersectTypesLegacy(): void + { + $this->expectUserDeprecationMessage('Since api-platform/metadata 4.2: The "ApiPlatform\Metadata\ApiProperty::withBuiltinTypes()" method is deprecated, use "ApiPlatform\Metadata\ApiProperty::withNativeType()" instead.'); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(NotAResourceWithUnionIntersectTypes::class, Argument::cetera())->willReturn(new PropertyNameCollection(['ignoredProperty', 'unionType', 'intersectType'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(NotAResourceWithUnionIntersectTypes::class, 'ignoredProperty', Argument::cetera())->willReturn( + (new ApiProperty()) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING, nullable: true)]) + ->withReadable(true) + ->withSchema(['type' => ['string', 'null']]) + ); + $propertyMetadataFactoryProphecy->create(NotAResourceWithUnionIntersectTypes::class, 'unionType', Argument::cetera())->willReturn( + (new ApiProperty()) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING, nullable: true), new LegacyType(LegacyType::BUILTIN_TYPE_INT, nullable: true), new LegacyType(LegacyType::BUILTIN_TYPE_FLOAT, nullable: true)]) + ->withReadable(true) + ->withSchema(['oneOf' => [ + ['type' => ['string', 'null']], + ['type' => ['integer', 'null']], + ]]) + ); + $propertyMetadataFactoryProphecy->create(NotAResourceWithUnionIntersectTypes::class, 'intersectType', Argument::cetera())->willReturn( + (new ApiProperty()) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, class: Serializable::class), new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, class: DummyResourceInterface::class)]) + ->withReadable(true) + ->withSchema(['type' => 'object']) + ); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(NotAResourceWithUnionIntersectTypes::class)->willReturn(false); + + $definitionNameFactory = new DefinitionNameFactory(); + + $schemaFactory = new SchemaFactory( + resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), + propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), + resourceClassResolver: $resourceClassResolverProphecy->reveal(), + definitionNameFactory: $definitionNameFactory, + ); + $resultSchema = $schemaFactory->buildSchema(NotAResourceWithUnionIntersectTypes::class); + + $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); + $definitions = $resultSchema->getDefinitions(); + + $this->assertSame((new \ReflectionClass(NotAResourceWithUnionIntersectTypes::class))->getShortName(), $rootDefinitionKey); + $this->assertTrue(isset($definitions[$rootDefinitionKey])); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]); + $this->assertSame('object', $definitions[$rootDefinitionKey]['type']); + $this->assertArrayNotHasKey('additionalProperties', $definitions[$rootDefinitionKey]); + $this->assertArrayHasKey('properties', $definitions[$rootDefinitionKey]); + + $this->assertArrayHasKey('ignoredProperty', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['ignoredProperty']); + $this->assertSame(['string', 'null'], $definitions[$rootDefinitionKey]['properties']['ignoredProperty']['type']); + $this->assertArrayHasKey('unionType', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('oneOf', $definitions[$rootDefinitionKey]['properties']['unionType']); + $this->assertCount(2, $definitions[$rootDefinitionKey]['properties']['unionType']['oneOf']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['unionType']['oneOf'][0]); + $this->assertSame(['string', 'null'], $definitions[$rootDefinitionKey]['properties']['unionType']['oneOf'][0]['type']); + $this->assertSame(['integer', 'null'], $definitions[$rootDefinitionKey]['properties']['unionType']['oneOf'][1]['type']); + + $this->assertArrayHasKey('intersectType', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['intersectType']); + $this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['intersectType']['type']); + } + public function testBuildSchemaForNonResourceClassWithUnionIntersectTypes(): void { $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); @@ -128,13 +305,13 @@ public function testBuildSchemaForNonResourceClassWithUnionIntersectTypes(): voi $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(NotAResourceWithUnionIntersectTypes::class, 'ignoredProperty', Argument::cetera())->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING, nullable: true)]) + ->withNativeType(Type::nullable(Type::string())) ->withReadable(true) ->withSchema(['type' => ['string', 'null']]) ); $propertyMetadataFactoryProphecy->create(NotAResourceWithUnionIntersectTypes::class, 'unionType', Argument::cetera())->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING, nullable: true), new Type(Type::BUILTIN_TYPE_INT, nullable: true), new Type(Type::BUILTIN_TYPE_FLOAT, nullable: true)]) + ->withNativeType(Type::union(Type::string(), Type::int(), Type::float(), Type::null())) ->withReadable(true) ->withSchema(['oneOf' => [ ['type' => ['string', 'null']], @@ -143,7 +320,7 @@ public function testBuildSchemaForNonResourceClassWithUnionIntersectTypes(): voi ); $propertyMetadataFactoryProphecy->create(NotAResourceWithUnionIntersectTypes::class, 'intersectType', Argument::cetera())->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, class: Serializable::class), new Type(Type::BUILTIN_TYPE_OBJECT, class: DummyResourceInterface::class)]) + ->withNativeType(Type::intersection(Type::object(Serializable::class), Type::object(DummyResourceInterface::class))) ->withReadable(true) ->withSchema(['type' => 'object']) ); @@ -151,10 +328,9 @@ public function testBuildSchemaForNonResourceClassWithUnionIntersectTypes(): voi $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(NotAResourceWithUnionIntersectTypes::class)->willReturn(false); - $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + $definitionNameFactory = new DefinitionNameFactory(); $schemaFactory = new SchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), @@ -189,8 +365,10 @@ public function testBuildSchemaForNonResourceClassWithUnionIntersectTypes(): voi $this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['intersectType']['type']); } - public function testBuildSchemaWithSerializerGroups(): void + #[IgnoreDeprecations] + public function testBuildSchemaWithSerializerGroupsLegacy(): void { + $this->expectUserDeprecationMessage('Since api-platform/metadata 4.2: The "ApiPlatform\Metadata\ApiProperty::withBuiltinTypes()" method is deprecated, use "ApiPlatform\Metadata\ApiProperty::withNativeType()" instead.'); $shortName = (new \ReflectionClass(OverriddenOperationDummy::class))->getShortName(); $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $operation = (new Put())->withName('put')->withNormalizationContext([ @@ -212,19 +390,19 @@ public function testBuildSchemaWithSerializerGroups(): void $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'alias', Argument::type('array'))->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]) ->withReadable(true) ->withSchema(['type' => 'string']) ); $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'description', Argument::type('array'))->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)]) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)]) ->withReadable(true) ->withSchema(['type' => 'string']) ); $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'genderType', Argument::type('array'))->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]) ->withReadable(true) ->withDefault(GenderTypeEnum::MALE) ->withSchema(['type' => 'object']) @@ -234,10 +412,86 @@ public function testBuildSchemaWithSerializerGroups(): void $resourceClassResolverProphecy->isResourceClass(OverriddenOperationDummy::class)->willReturn(true); $resourceClassResolverProphecy->isResourceClass(GenderTypeEnum::class)->willReturn(true); - $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + $definitionNameFactory = new DefinitionNameFactory(); + + $schemaFactory = new SchemaFactory( + resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), + propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), + resourceClassResolver: $resourceClassResolverProphecy->reveal(), + definitionNameFactory: $definitionNameFactory, + ); + $resultSchema = $schemaFactory->buildSchema(OverriddenOperationDummy::class, 'json', Schema::TYPE_OUTPUT, null, null, ['groups' => $serializerGroup, AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false]); + + $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); + $definitions = $resultSchema->getDefinitions(); + + $this->assertSame((new \ReflectionClass(OverriddenOperationDummy::class))->getShortName().'-'.$serializerGroup, $rootDefinitionKey); + $this->assertTrue(isset($definitions[$rootDefinitionKey])); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]); + $this->assertSame('object', $definitions[$rootDefinitionKey]['type']); + $this->assertFalse($definitions[$rootDefinitionKey]['additionalProperties']); + $this->assertArrayHasKey('properties', $definitions[$rootDefinitionKey]); + $this->assertArrayHasKey('alias', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['alias']); + $this->assertSame('string', $definitions[$rootDefinitionKey]['properties']['alias']['type']); + $this->assertArrayHasKey('description', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['description']); + $this->assertSame('string', $definitions[$rootDefinitionKey]['properties']['description']['type']); + $this->assertArrayHasKey('genderType', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayNotHasKey('default', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertArrayNotHasKey('example', $definitions[$rootDefinitionKey]['properties']['genderType']); + $this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['genderType']['type']); + } + + public function testBuildSchemaWithSerializerGroups(): void + { + $shortName = (new \ReflectionClass(OverriddenOperationDummy::class))->getShortName(); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $operation = (new Put())->withName('put')->withNormalizationContext([ + 'groups' => 'overridden_operation_dummy_put', + AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false, + ])->withShortName($shortName)->withValidationContext(['groups' => ['validation_groups_dummy_put']]); + $resourceMetadataFactoryProphecy->create(OverriddenOperationDummy::class) + ->willReturn( + new ResourceMetadataCollection(OverriddenOperationDummy::class, [ + (new ApiResource())->withOperations(new Operations(['put' => $operation])), + ]) + ); + + $serializerGroup = 'custom_operation_dummy'; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(OverriddenOperationDummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['alias', 'description', 'genderType'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'alias', Argument::type('array'))->willReturn( + (new ApiProperty()) + ->withNativeType(Type::string()) + ->withReadable(true) + ->withSchema(['type' => 'string']) + ); + $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'description', Argument::type('array'))->willReturn( + (new ApiProperty()) + ->withNativeType(Type::string()) + ->withReadable(true) + ->withSchema(['type' => 'string']) + ); + $propertyMetadataFactoryProphecy->create(OverriddenOperationDummy::class, 'genderType', Argument::type('array'))->willReturn( + (new ApiProperty()) + ->withNativeType(Type::enum(GenderTypeEnum::class)) + ->withReadable(true) + ->withDefault(GenderTypeEnum::MALE) + ->withSchema(['type' => 'object']) + ); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(OverriddenOperationDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(GenderTypeEnum::class)->willReturn(true); + + $definitionNameFactory = new DefinitionNameFactory(); $schemaFactory = new SchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), @@ -268,6 +522,60 @@ public function testBuildSchemaWithSerializerGroups(): void $this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['genderType']['type']); } + #[IgnoreDeprecations] + public function testBuildSchemaForAssociativeArrayLegacy(): void + { + $this->expectUserDeprecationMessage('Since api-platform/metadata 4.2: The "ApiPlatform\Metadata\ApiProperty::withBuiltinTypes()" method is deprecated, use "ApiPlatform\Metadata\ApiProperty::withNativeType()" instead.'); + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(NotAResource::class, Argument::cetera())->willReturn(new PropertyNameCollection(['foo', 'bar'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(NotAResource::class, 'foo', Argument::cetera())->willReturn( + (new ApiProperty()) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_INT), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]) + ->withReadable(true) + ->withSchema(['type' => 'array', 'items' => ['string', 'int']]) + ); + $propertyMetadataFactoryProphecy->create(NotAResource::class, 'bar', Argument::cetera())->willReturn( + (new ApiProperty()) + ->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_ARRAY, false, null, true, new LegacyType(LegacyType::BUILTIN_TYPE_STRING), new LegacyType(LegacyType::BUILTIN_TYPE_STRING))]) + ->withReadable(true) + ->withSchema(['type' => 'object', 'additionalProperties' => 'string']) + ); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false); + + $definitionNameFactory = new DefinitionNameFactory(); + + $schemaFactory = new SchemaFactory( + resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), + propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), + propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), + resourceClassResolver: $resourceClassResolverProphecy->reveal(), + definitionNameFactory: $definitionNameFactory, + ); + $resultSchema = $schemaFactory->buildSchema(NotAResource::class); + + $rootDefinitionKey = $resultSchema->getRootDefinitionKey(); + $definitions = $resultSchema->getDefinitions(); + + $this->assertSame((new \ReflectionClass(NotAResource::class))->getShortName(), $rootDefinitionKey); + $this->assertTrue(isset($definitions[$rootDefinitionKey])); + $this->assertArrayHasKey('properties', $definitions[$rootDefinitionKey]); + $this->assertArrayHasKey('foo', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['foo']); + $this->assertArrayNotHasKey('additionalProperties', $definitions[$rootDefinitionKey]['properties']['foo']); + $this->assertSame('array', $definitions[$rootDefinitionKey]['properties']['foo']['type']); + $this->assertArrayHasKey('bar', $definitions[$rootDefinitionKey]['properties']); + $this->assertArrayHasKey('type', $definitions[$rootDefinitionKey]['properties']['bar']); + $this->assertArrayHasKey('additionalProperties', $definitions[$rootDefinitionKey]['properties']['bar']); + $this->assertSame('object', $definitions[$rootDefinitionKey]['properties']['bar']['type']); + $this->assertSame('string', $definitions[$rootDefinitionKey]['properties']['bar']['additionalProperties']); + } + public function testBuildSchemaForAssociativeArray(): void { $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); @@ -278,13 +586,13 @@ public function testBuildSchemaForAssociativeArray(): void $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(NotAResource::class, 'foo', Argument::cetera())->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]) + ->withNativeType(Type::list(Type::string())) ->withReadable(true) ->withSchema(['type' => 'array', 'items' => ['string', 'int']]) ); $propertyMetadataFactoryProphecy->create(NotAResource::class, 'bar', Argument::cetera())->willReturn( (new ApiProperty()) - ->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_STRING))]) + ->withNativeType(Type::dict(Type::string())) ->withReadable(true) ->withSchema(['type' => 'object', 'additionalProperties' => 'string']) ); @@ -292,10 +600,9 @@ public function testBuildSchemaForAssociativeArray(): void $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false); - $definitionNameFactory = new DefinitionNameFactory(['jsonapi' => true, 'jsonhal' => true, 'jsonld' => true]); + $definitionNameFactory = new DefinitionNameFactory(); $schemaFactory = new SchemaFactory( - typeFactory: null, resourceMetadataFactory: $resourceMetadataFactoryProphecy->reveal(), propertyNameCollectionFactory: $propertyNameCollectionFactoryProphecy->reveal(), propertyMetadataFactory: $propertyMetadataFactoryProphecy->reveal(), diff --git a/src/JsonSchema/Tests/SchemaTest.php b/src/JsonSchema/Tests/SchemaTest.php index 73175d0995a..d60ab82a6ba 100644 --- a/src/JsonSchema/Tests/SchemaTest.php +++ b/src/JsonSchema/Tests/SchemaTest.php @@ -18,9 +18,7 @@ class SchemaTest extends TestCase { - /** - * @dataProvider versionProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('versionProvider')] public function testJsonSchemaVersion(string $version, string $ref): void { $schema = new Schema($version); @@ -31,9 +29,7 @@ public function testJsonSchemaVersion(string $version, string $ref): void $this->assertSame('Foo', $schema->getRootDefinitionKey()); } - /** - * @dataProvider versionProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('versionProvider')] public function testCollectionJsonSchemaVersion(string $version, string $ref): void { $schema = new Schema($version); @@ -62,9 +58,7 @@ public function testContainsJsonSchemaVersion(): void $this->assertSame('http://json-schema.org/draft-07/schema#', $schema['$schema']); } - /** - * @dataProvider definitionsDataProvider - */ + #[\PHPUnit\Framework\Attributes\DataProvider('definitionsDataProvider')] public function testDefinitions(string $version, array $baseDefinitions): void { $schema = new Schema($version); diff --git a/src/JsonSchema/Tests/TypeFactoryTest.php b/src/JsonSchema/Tests/TypeFactoryTest.php deleted file mode 100644 index e1a5a0cd8cc..00000000000 --- a/src/JsonSchema/Tests/TypeFactoryTest.php +++ /dev/null @@ -1,472 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\JsonSchema\Tests; - -use ApiPlatform\JsonSchema\Schema; -use ApiPlatform\JsonSchema\SchemaFactoryInterface; -use ApiPlatform\JsonSchema\Tests\Fixtures\ApiResource\Dummy; -use ApiPlatform\JsonSchema\Tests\Fixtures\Enum\GamePlayMode; -use ApiPlatform\JsonSchema\Tests\Fixtures\Enum\GenderTypeEnum; -use ApiPlatform\JsonSchema\TypeFactory; -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\PropertyInfo\Type; - -class TypeFactoryTest extends TestCase -{ - use ProphecyTrait; - - /** - * @dataProvider typeProvider - */ - public function testGetType(array $schema, Type $type): void - { - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); - $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); - $typeFactory = new TypeFactory($resourceClassResolver->reveal()); - $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_OPENAPI))); - } - - public static function typeProvider(): iterable - { - yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT)]; - yield [['nullable' => true, 'type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT, true)]; - yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT)]; - yield [['nullable' => true, 'type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT, true)]; - yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL)]; - yield [['nullable' => true, 'type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING)]; - yield [['nullable' => true, 'type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT)]; - yield [['nullable' => true, 'type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT, true)]; - yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]; - yield [['nullable' => true, 'type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)]; - yield [['type' => 'string', 'format' => 'duration'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateInterval::class)]; - yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; - yield [['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; - yield [['nullable' => true, 'type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield ['nullable enum' => ['type' => 'string', 'enum' => ['male', 'female', null], 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; - yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; - yield ['nullable enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/', 'nullable' => true], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; - yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; - yield 'array can be itself nullable' => [ - ['nullable' => true, 'type' => 'array', 'items' => ['type' => 'string']], - new Type(Type::BUILTIN_TYPE_STRING, true, null, true), - ]; - - yield 'array can contain nullable values' => [ - [ - 'type' => 'array', - 'items' => [ - 'nullable' => true, - 'type' => 'string', - ], - ], - new Type(Type::BUILTIN_TYPE_STRING, false, null, true, null, new Type(Type::BUILTIN_TYPE_STRING, true, null, false)), - ]; - - yield 'map with string keys becomes an object' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'string']], - new Type( - Type::BUILTIN_TYPE_STRING, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'nullable map with string keys becomes a nullable object' => [ - [ - 'nullable' => true, - 'type' => 'object', - 'additionalProperties' => ['type' => 'string'], - ], - new Type( - Type::BUILTIN_TYPE_STRING, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'map value type will be considered' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'integer']], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, false, null, false) - ), - ]; - - yield 'map value type nullability will be considered' => [ - [ - 'type' => 'object', - 'additionalProperties' => [ - 'nullable' => true, - 'type' => 'integer', - ], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - - yield 'nullable map can contain nullable values' => [ - [ - 'nullable' => true, - 'type' => 'object', - 'additionalProperties' => [ - 'nullable' => true, - 'type' => 'integer', - ], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - } - - /** - * @dataProvider jsonSchemaTypeProvider - */ - public function testGetTypeWithJsonSchemaSyntax(array $schema, Type $type): void - { - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); - $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); - $typeFactory = new TypeFactory($resourceClassResolver->reveal()); - $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_JSON_SCHEMA))); - } - - public static function jsonSchemaTypeProvider(): iterable - { - yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT)]; - yield [['type' => ['integer', 'null']], new Type(Type::BUILTIN_TYPE_INT, true)]; - yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT)]; - yield [['type' => ['number', 'null']], new Type(Type::BUILTIN_TYPE_FLOAT, true)]; - yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL)]; - yield [['type' => ['boolean', 'null']], new Type(Type::BUILTIN_TYPE_BOOL, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING)]; - yield [['type' => ['string', 'null']], new Type(Type::BUILTIN_TYPE_STRING, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT)]; - yield [['type' => ['string', 'null']], new Type(Type::BUILTIN_TYPE_OBJECT, true)]; - yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]; - yield [['type' => ['string', 'null'], 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)]; - yield [['type' => 'string', 'format' => 'duration'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateInterval::class)]; - yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; - yield [['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; - yield [['type' => ['string', 'null'], 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield ['nullable enum' => ['type' => ['string', 'null'], 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; - yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; - yield ['nullable enum resource' => ['type' => ['string', 'null'], 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; - yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; - yield 'array can be itself nullable' => [ - ['type' => ['array', 'null'], 'items' => ['type' => 'string']], - new Type(Type::BUILTIN_TYPE_STRING, true, null, true), - ]; - - yield 'array can contain nullable values' => [ - [ - 'type' => 'array', - 'items' => [ - 'type' => ['string', 'null'], - ], - ], - new Type(Type::BUILTIN_TYPE_STRING, false, null, true, null, new Type(Type::BUILTIN_TYPE_STRING, true, null, false)), - ]; - - yield 'map with string keys becomes an object' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'string']], - new Type( - Type::BUILTIN_TYPE_STRING, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'nullable map with string keys becomes a nullable object' => [ - [ - 'type' => ['object', 'null'], - 'additionalProperties' => ['type' => 'string'], - ], - new Type( - Type::BUILTIN_TYPE_STRING, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'map value type will be considered' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'integer']], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, false, null, false) - ), - ]; - - yield 'map value type nullability will be considered' => [ - [ - 'type' => 'object', - 'additionalProperties' => [ - 'type' => ['integer', 'null'], - ], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - - yield 'nullable map can contain nullable values' => [ - [ - 'type' => ['object', 'null'], - 'additionalProperties' => [ - 'type' => ['integer', 'null'], - ], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - } - - /** @dataProvider openAPIV2TypeProvider */ - public function testGetTypeWithOpenAPIV2Syntax(array $schema, Type $type): void - { - $resourceClassResolver = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolver->isResourceClass(GenderTypeEnum::class)->willReturn(false); - $resourceClassResolver->isResourceClass(Argument::type('string'))->willReturn(true); - $typeFactory = new TypeFactory($resourceClassResolver->reveal()); - $this->assertEquals($schema, $typeFactory->getType($type, 'json', null, null, new Schema(Schema::VERSION_SWAGGER))); - } - - public static function openAPIV2TypeProvider(): iterable - { - yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT)]; - yield [['type' => 'integer'], new Type(Type::BUILTIN_TYPE_INT, true)]; - yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT)]; - yield [['type' => 'number'], new Type(Type::BUILTIN_TYPE_FLOAT, true)]; - yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL)]; - yield [['type' => 'boolean'], new Type(Type::BUILTIN_TYPE_BOOL, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_STRING, true)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT)]; - yield [['type' => 'string'], new Type(Type::BUILTIN_TYPE_OBJECT, true)]; - yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateTimeImmutable::class)]; - yield [['type' => 'string', 'format' => 'date-time'], new Type(Type::BUILTIN_TYPE_OBJECT, true, \DateTimeImmutable::class)]; - yield [['type' => 'string', 'format' => 'duration'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \DateInterval::class)]; - yield [['type' => 'string', 'format' => 'binary'], new Type(Type::BUILTIN_TYPE_OBJECT, false, \SplFileInfo::class)]; - yield [['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class)]; - yield [['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class)]; - yield ['enum' => ['type' => 'string', 'enum' => ['male', 'female']], new Type(Type::BUILTIN_TYPE_OBJECT, false, GenderTypeEnum::class)]; - yield ['nullable enum' => ['type' => 'string', 'enum' => ['male', 'female', null]], new Type(Type::BUILTIN_TYPE_OBJECT, true, GenderTypeEnum::class)]; - yield ['enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, false, GamePlayMode::class)]; - yield ['nullable enum resource' => ['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], new Type(Type::BUILTIN_TYPE_OBJECT, true, GamePlayMode::class)]; - yield [['type' => 'array', 'items' => ['type' => 'string']], new Type(Type::BUILTIN_TYPE_STRING, false, null, true)]; - yield 'array can be itself nullable, but ignored in OpenAPI V2' => [ - ['type' => 'array', 'items' => ['type' => 'string']], - new Type(Type::BUILTIN_TYPE_STRING, true, null, true), - ]; - - yield 'array can contain nullable values, but ignored in OpenAPI V2' => [ - [ - 'type' => 'array', - 'items' => ['type' => 'string'], - ], - new Type(Type::BUILTIN_TYPE_STRING, false, null, true, null, new Type(Type::BUILTIN_TYPE_STRING, true, null, false)), - ]; - - yield 'map with string keys becomes an object' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'string']], - new Type( - Type::BUILTIN_TYPE_STRING, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'nullable map with string keys becomes a nullable object, but ignored in OpenAPI V2' => [ - [ - 'type' => 'object', - 'additionalProperties' => ['type' => 'string'], - ], - new Type( - Type::BUILTIN_TYPE_STRING, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_STRING, false, null, false) - ), - ]; - - yield 'map value type will be considered' => [ - ['type' => 'object', 'additionalProperties' => ['type' => 'integer']], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, false, null, false) - ), - ]; - - yield 'map value type nullability will be considered, but ignored in OpenAPI V2' => [ - [ - 'type' => 'object', - 'additionalProperties' => ['type' => 'integer'], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - false, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - - yield 'nullable map can contain nullable values, but ignored in OpenAPI V2' => [ - [ - 'type' => 'object', - 'additionalProperties' => ['type' => 'integer'], - ], - new Type( - Type::BUILTIN_TYPE_ARRAY, - true, - null, - true, - new Type(Type::BUILTIN_TYPE_STRING, false, null, false), - new Type(Type::BUILTIN_TYPE_INT, true, null, false) - ), - ]; - } - - public function testGetClassType(): void - { - $schemaFactoryProphecy = $this->prophesize(SchemaFactoryInterface::class); - - $schemaFactoryProphecy->buildSchema(Dummy::class, 'jsonld', Schema::TYPE_OUTPUT, null, Argument::type(Schema::class), Argument::type('array'), false)->will(function (array $args) { - $args[4]['$ref'] = 'ref'; - - return $args[4]; - }); - - $typeFactory = new TypeFactory(); - $typeFactory->setSchemaFactory($schemaFactoryProphecy->reveal()); - - $this->assertEquals(['$ref' => 'ref'], $typeFactory->getType(new Type(Type::BUILTIN_TYPE_OBJECT, false, Dummy::class), 'jsonld', true, ['foo' => 'bar'], new Schema())); - } - - /** @dataProvider classTypeWithNullabilityDataProvider */ - public function testGetClassTypeWithNullability(array $expected, callable $schemaFactoryFactory, Schema $schema): void - { - $typeFactory = new TypeFactory(); - $typeFactory->setSchemaFactory($schemaFactoryFactory($this)); - - self::assertEquals( - $expected, - $typeFactory->getType(new Type(Type::BUILTIN_TYPE_OBJECT, true, Dummy::class), 'jsonld', true, ['foo' => 'bar'], $schema) - ); - } - - public static function classTypeWithNullabilityDataProvider(): iterable - { - $schema = new Schema(); - $schemaFactoryFactory = fn (self $that): SchemaFactoryInterface => $that->createSchemaFactoryMock($schema); - - yield 'JSON-Schema version' => [ - [ - 'anyOf' => [ - ['$ref' => 'the-ref-name'], - ['type' => 'null'], - ], - ], - $schemaFactoryFactory, - $schema, - ]; - - $schema = new Schema(Schema::VERSION_OPENAPI); - $schemaFactoryFactory = fn (self $that): SchemaFactoryInterface => $that->createSchemaFactoryMock($schema); - - yield 'OpenAPI < 3.1 version' => [ - [ - 'anyOf' => [ - ['$ref' => 'the-ref-name'], - ], - 'nullable' => true, - ], - $schemaFactoryFactory, - $schema, - ]; - } - - private function createSchemaFactoryMock(Schema $schema): SchemaFactoryInterface - { - $schemaFactory = $this->createMock(SchemaFactoryInterface::class); - - $schemaFactory - ->method('buildSchema') - ->willReturnCallback(static function () use ($schema): Schema { - $schema['$ref'] = 'the-ref-name'; - $schema['description'] = 'more stuff here'; - - return $schema; - }); - - return $schemaFactory; - } -} diff --git a/src/JsonSchema/TypeFactory.php b/src/JsonSchema/TypeFactory.php deleted file mode 100644 index 3921913e82e..00000000000 --- a/src/JsonSchema/TypeFactory.php +++ /dev/null @@ -1,207 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\JsonSchema; - -use ApiPlatform\Metadata\ResourceClassResolverInterface; -use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; -use Ramsey\Uuid\UuidInterface; -use Symfony\Component\PropertyInfo\Type; -use Symfony\Component\Uid\Ulid; -use Symfony\Component\Uid\Uuid; - -/** - * {@inheritdoc} - * - * @deprecated since 3.3 https://github.com/api-platform/core/pull/5470 - * - * @author Kévin Dunglas - */ -final class TypeFactory implements TypeFactoryInterface -{ - use ResourceClassInfoTrait; - - private ?SchemaFactoryInterface $schemaFactory = null; - - public function __construct(?ResourceClassResolverInterface $resourceClassResolver = null) - { - $this->resourceClassResolver = $resourceClassResolver; - } - - public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void - { - $this->schemaFactory = $schemaFactory; - } - - /** - * {@inheritdoc} - */ - public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, ?Schema $schema = null): array - { - if ('jsonschema' === $format) { - return []; - } - - // TODO: OpenApiFactory uses this to compute filter types - if ($type->isCollection()) { - $keyType = $type->getCollectionKeyTypes()[0] ?? null; - $subType = ($type->getCollectionValueTypes()[0] ?? null) ?? new Type($type->getBuiltinType(), false, $type->getClassName(), false); - - if (null !== $keyType && Type::BUILTIN_TYPE_STRING === $keyType->getBuiltinType()) { - return $this->addNullabilityToTypeDefinition([ - 'type' => 'object', - 'additionalProperties' => $this->getType($subType, $format, $readableLink, $serializerContext, $schema), - ], $type, $schema); - } - - return $this->addNullabilityToTypeDefinition([ - 'type' => 'array', - 'items' => $this->getType($subType, $format, $readableLink, $serializerContext, $schema), - ], $type, $schema); - } - - return $this->addNullabilityToTypeDefinition($this->makeBasicType($type, $format, $readableLink, $serializerContext, $schema), $type, $schema); - } - - private function makeBasicType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, ?Schema $schema = null): array - { - return match ($type->getBuiltinType()) { - Type::BUILTIN_TYPE_INT => ['type' => 'integer'], - Type::BUILTIN_TYPE_FLOAT => ['type' => 'number'], - Type::BUILTIN_TYPE_BOOL => ['type' => 'boolean'], - Type::BUILTIN_TYPE_OBJECT => $this->getClassType($type->getClassName(), $type->isNullable(), $format, $readableLink, $serializerContext, $schema), - default => ['type' => 'string'], - }; - } - - /** - * Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided. - */ - private function getClassType(?string $className, bool $nullable, string $format, ?bool $readableLink, ?array $serializerContext, ?Schema $schema): array - { - if (null === $className) { - return ['type' => 'string']; - } - - if (is_a($className, \DateTimeInterface::class, true)) { - return [ - 'type' => 'string', - 'format' => 'date-time', - ]; - } - if (is_a($className, \DateInterval::class, true)) { - return [ - 'type' => 'string', - 'format' => 'duration', - ]; - } - if (is_a($className, UuidInterface::class, true) || is_a($className, Uuid::class, true)) { - return [ - 'type' => 'string', - 'format' => 'uuid', - ]; - } - if (is_a($className, Ulid::class, true)) { - return [ - 'type' => 'string', - 'format' => 'ulid', - ]; - } - if (is_a($className, \SplFileInfo::class, true)) { - return [ - 'type' => 'string', - 'format' => 'binary', - ]; - } - if (!$this->isResourceClass($className) && is_a($className, \BackedEnum::class, true)) { - $enumCases = array_map(static fn (\BackedEnum $enum): string|int => $enum->value, $className::cases()); - - $type = \is_string($enumCases[0] ?? '') ? 'string' : 'integer'; - - if ($nullable) { - $enumCases[] = null; - } - - return [ - 'type' => $type, - 'enum' => $enumCases, - ]; - } - - // Skip if $schema is null (filters only support basic types) - if (null === $schema) { - return ['type' => 'string']; - } - - if (true !== $readableLink && $this->isResourceClass($className)) { - return [ - 'type' => 'string', - 'format' => 'iri-reference', - 'example' => 'https://example.com/', - ]; - } - - $version = $schema->getVersion(); - - $subSchema = new Schema($version); - $subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema - - if (null === $this->schemaFactory) { - throw new \LogicException('The schema factory must be injected by calling the "setSchemaFactory" method.'); - } - - $serializerContext += [SchemaFactory::FORCE_SUBSCHEMA => true]; - $subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, $subSchema, $serializerContext, false); - - return ['$ref' => $subSchema['$ref']]; - } - - /** - * @param array $jsonSchema - * - * @return array - */ - private function addNullabilityToTypeDefinition(array $jsonSchema, Type $type, ?Schema $schema): array - { - if ($schema && Schema::VERSION_SWAGGER === $schema->getVersion()) { - return $jsonSchema; - } - - if (!$type->isNullable()) { - return $jsonSchema; - } - - if (\array_key_exists('$ref', $jsonSchema)) { - $typeDefinition = ['anyOf' => [$jsonSchema]]; - - if ($schema && Schema::VERSION_JSON_SCHEMA === $schema->getVersion()) { - $typeDefinition['anyOf'][] = ['type' => 'null']; - } else { - // OpenAPI < 3.1 - $typeDefinition['nullable'] = true; - } - - return $typeDefinition; - } - - if ($schema && Schema::VERSION_JSON_SCHEMA === $schema->getVersion()) { - return [...$jsonSchema, ...[ - 'type' => \is_array($jsonSchema['type']) - ? array_merge($jsonSchema['type'], ['null']) - : [$jsonSchema['type'], 'null'], - ]]; - } - - return [...$jsonSchema, ...['nullable' => true]]; - } -} diff --git a/src/JsonSchema/TypeFactoryInterface.php b/src/JsonSchema/TypeFactoryInterface.php deleted file mode 100644 index b2ba889c14e..00000000000 --- a/src/JsonSchema/TypeFactoryInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace ApiPlatform\JsonSchema; - -use Symfony\Component\PropertyInfo\Type; - -/** - * Factory for creating the JSON Schema document which specifies the data type corresponding to a PHP type. - * - * @author Kévin Dunglas - */ -interface TypeFactoryInterface -{ - /** - * Gets the JSON Schema document which specifies the data type corresponding to the given PHP type, and recursively adds needed new schema to the current schema if provided. - */ - public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, ?Schema $schema = null): array; -} diff --git a/src/JsonSchema/composer.json b/src/JsonSchema/composer.json index 69133a14cb5..948c64ddeb8 100644 --- a/src/JsonSchema/composer.json +++ b/src/JsonSchema/composer.json @@ -24,17 +24,17 @@ } ], "require": { - "php": ">=8.1", - "api-platform/metadata": "*@dev || ^3.1", + "php": ">=8.2", + "api-platform/metadata": "^4.2", "symfony/console": "^6.4 || ^7.0", - "symfony/property-info": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.1", "symfony/serializer": "^6.4 || ^7.0", + "symfony/type-info": "^7.3", "symfony/uid": "^6.4 || ^7.0" }, "require-dev": { - "phpspec/prophecy-phpunit": "^2.0", - "symfony/phpunit-bridge": "^6.4 || ^7.0", - "sebastian/comparator": "<5.0" + "phpspec/prophecy-phpunit": "^2.2", + "phpunit/phpunit": "11.5.x-dev" }, "autoload": { "psr-4": { @@ -56,13 +56,26 @@ }, "extra": { "branch-alias": { - "dev-main": "3.3.x-dev" + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" }, "symfony": { - "require": "^6.4" + "require": "^6.4 || ^7.0" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" } }, "scripts": { "test": "./vendor/bin/phpunit" - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] } diff --git a/src/JsonSchema/phpunit.baseline.xml b/src/JsonSchema/phpunit.baseline.xml new file mode 100644 index 00000000000..e3ef6196399 --- /dev/null +++ b/src/JsonSchema/phpunit.baseline.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/JsonSchema/phpunit.xml.dist b/src/JsonSchema/phpunit.xml.dist index b33b9e34a4a..1987ee6491c 100644 --- a/src/JsonSchema/phpunit.xml.dist +++ b/src/JsonSchema/phpunit.xml.dist @@ -1,31 +1,23 @@ - - - - - - - - - ./Tests/ - - - - - - ./ - - - ./Tests - ./vendor - - + + + + + + + ./Tests/ + + + + + trigger_deprecation + + + ./ + + + ./Tests + ./vendor + + - diff --git a/src/Laravel/.gitattributes b/src/Laravel/.gitattributes new file mode 100644 index 00000000000..af1bba3d5e4 --- /dev/null +++ b/src/Laravel/.gitattributes @@ -0,0 +1,8 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/CONTRIBUTING.md +/testbench.yaml +/Tests export-ignore +/workbench export-ignore diff --git a/src/Laravel/.github/workflows/close_pr.yml b/src/Laravel/.github/workflows/close_pr.yml new file mode 100644 index 00000000000..72a8ab4325e --- /dev/null +++ b/src/Laravel/.github/workflows/close_pr.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on a read-only sub split of `api-platform/core`. Please submit your PR on the https://github.com/api-platform/core repository.

Thanks!" diff --git a/src/Laravel/.gitignore b/src/Laravel/.gitignore new file mode 100644 index 00000000000..eb0a8e7b262 --- /dev/null +++ b/src/Laravel/.gitignore @@ -0,0 +1,3 @@ +/composer.lock +/vendor +/.phpunit.result.cache diff --git a/src/Laravel/ApiPlatformDeferredProvider.php b/src/Laravel/ApiPlatformDeferredProvider.php new file mode 100644 index 00000000000..227e2f9c680 --- /dev/null +++ b/src/Laravel/ApiPlatformDeferredProvider.php @@ -0,0 +1,361 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel; + +use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; +use ApiPlatform\GraphQl\State\Provider\DenormalizeProvider as GraphQlDenormalizeProvider; +use ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface; +use ApiPlatform\GraphQl\Type\FieldsBuilder; +use ApiPlatform\GraphQl\Type\FieldsBuilderEnumInterface; +use ApiPlatform\GraphQl\Type\TypeConverterInterface; +use ApiPlatform\GraphQl\Type\TypesContainerInterface; +use ApiPlatform\JsonApi\Filter\SparseFieldset; +use ApiPlatform\JsonApi\Filter\SparseFieldsetParameterProvider; +use ApiPlatform\Laravel\Controller\ApiPlatformController; +use ApiPlatform\Laravel\Eloquent\Extension\FilterQueryExtension; +use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface; +use ApiPlatform\Laravel\Eloquent\Filter\BooleanFilter; +use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; +use ApiPlatform\Laravel\Eloquent\Filter\EndSearchFilter; +use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter; +use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface as EloquentFilterInterface; +use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilter; +use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilterParameterProvider; +use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; +use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; +use ApiPlatform\Laravel\Eloquent\Filter\RangeFilter; +use ApiPlatform\Laravel\Eloquent\Filter\StartSearchFilter; +use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Resource\EloquentResourceCollectionMetadataFactory; +use ApiPlatform\Laravel\Eloquent\State\CollectionProvider; +use ApiPlatform\Laravel\Eloquent\State\ItemProvider; +use ApiPlatform\Laravel\Eloquent\State\LinksHandler; +use ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface; +use ApiPlatform\Laravel\Eloquent\State\PersistProcessor; +use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor; +use ApiPlatform\Laravel\Exception\ErrorHandler; +use ApiPlatform\Laravel\Metadata\CacheResourceCollectionMetadataFactory; +use ApiPlatform\Laravel\Metadata\ParameterValidationResourceMetadataCollectionFactory; +use ApiPlatform\Laravel\State\ParameterValidatorProvider; +use ApiPlatform\Laravel\State\SwaggerUiProcessor; +use ApiPlatform\Laravel\State\ValidateProvider; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; +use ApiPlatform\Metadata\InflectorInterface; +use ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\AlternateUriResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\AttributesResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\ConcernsResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\FiltersResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\FormatsResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\InputOutputResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\LinkFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\LinkResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\NotExposedOperationResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\OperationNameResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\ParameterResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\PhpDocResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\UriTemplateResourceMetadataCollectionFactory; +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\ReflectionClassRecursiveIterator; +use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface; +use ApiPlatform\Serializer\Filter\PropertyFilter; +use ApiPlatform\Serializer\Parameter\SerializerFilterParameterProvider; +use ApiPlatform\State\CallableProcessor; +use ApiPlatform\State\CallableProvider; +use ApiPlatform\State\ErrorProvider; +use ApiPlatform\State\Pagination\Pagination; +use ApiPlatform\State\ParameterProviderInterface; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\Provider\ParameterProvider; +use ApiPlatform\State\Provider\SecurityParameterProvider; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Contracts\Debug\ExceptionHandler; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Contracts\Support\DeferrableProvider; +use Illuminate\Support\ServiceProvider; +use Negotiation\Negotiator; +use Psr\Log\LoggerInterface; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +class ApiPlatformDeferredProvider extends ServiceProvider implements DeferrableProvider +{ + /** + * Register any application services. + */ + public function register(): void + { + $directory = app_path(); + $classes = ReflectionClassRecursiveIterator::getReflectionClassesFromDirectories([$directory], '(?!.*Test\.php$)'); + + $this->autoconfigure($classes, QueryExtensionInterface::class, [FilterQueryExtension::class]); + $this->app->singleton(ItemProvider::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class)); + + return new ItemProvider(new LinksHandler($app, $app->make(ResourceMetadataCollectionFactoryInterface::class)), new ServiceLocator($tagged), $app->tagged(QueryExtensionInterface::class)); + }); + + $this->app->singleton(CollectionProvider::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(LinksHandlerInterface::class)); + + return new CollectionProvider($app->make(Pagination::class), new LinksHandler($app, $app->make(ResourceMetadataCollectionFactoryInterface::class)), $app->tagged(QueryExtensionInterface::class), new ServiceLocator($tagged)); + }); + + $this->app->singleton(SerializerFilterParameterProvider::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(SerializerFilterInterface::class)); + + return new SerializerFilterParameterProvider(new ServiceLocator($tagged)); + }); + $this->app->alias(SerializerFilterParameterProvider::class, 'api_platform.serializer.filter_parameter_provider'); + + $this->app->singleton('filters', function (Application $app) { + return new ServiceLocator(array_merge( + iterator_to_array($app->tagged(SerializerFilterInterface::class)), + iterator_to_array($app->tagged(EloquentFilterInterface::class)) + )); + }); + + $this->autoconfigure($classes, SerializerFilterInterface::class, [PropertyFilter::class]); + + $this->app->singleton(ParameterProvider::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class)); + $tagged['api_platform.serializer.filter_parameter_provider'] = $app->make(SerializerFilterParameterProvider::class); + + return new ParameterProvider( + new ParameterValidatorProvider( + new SecurityParameterProvider( + $app->make(ValidateProvider::class), + $app->make(ResourceAccessCheckerInterface::class) + ), + ), + new ServiceLocator($tagged) + ); + }); + + $this->autoconfigure($classes, ParameterProviderInterface::class, [SerializerFilterParameterProvider::class, SortFilterParameterProvider::class, SparseFieldsetParameterProvider::class]); + + $this->app->bind(FilterQueryExtension::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(EloquentFilterInterface::class)); + + return new FilterQueryExtension(new ServiceLocator($tagged)); + }); + + $this->autoconfigure($classes, EloquentFilterInterface::class, [ + BooleanFilter::class, + DateFilter::class, + EndSearchFilter::class, + EqualsFilter::class, + OrderFilter::class, + PartialSearchFilter::class, + RangeFilter::class, + StartSearchFilter::class, + SortFilter::class, + SparseFieldset::class, + ]); + + $this->app->singleton(CallableProcessor::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $tagged = iterator_to_array($app->tagged(ProcessorInterface::class)); + + if ($config->get('api-platform.swagger_ui.enabled', false)) { + // TODO: tag SwaggerUiProcessor instead? + $tagged['api_platform.swagger_ui.processor'] = $app->make(SwaggerUiProcessor::class); + } + + return new CallableProcessor(new ServiceLocator($tagged)); + }); + + $this->autoconfigure($classes, ProcessorInterface::class, [RemoveProcessor::class, PersistProcessor::class]); + + $this->app->singleton(CallableProvider::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged(ProviderInterface::class)); + + return new CallableProvider(new ServiceLocator($tagged)); + }); + + $this->autoconfigure($classes, ProviderInterface::class, [ItemProvider::class, CollectionProvider::class, ErrorProvider::class]); + + $this->app->singleton(ResourceMetadataCollectionFactoryInterface::class, function (Application $app) { + /** @var ConfigRepository $config */ + $config = $app['config']; + $formats = $config->get('api-platform.formats'); + + if ($config->get('api-platform.swagger_ui.enabled', false) && !isset($formats['html'])) { + $formats['html'] = ['text/html']; + } + + return new CacheResourceCollectionMetadataFactory( + new EloquentResourceCollectionMetadataFactory( + new ParameterValidationResourceMetadataCollectionFactory( + new ParameterResourceMetadataCollectionFactory( + $this->app->make(PropertyNameCollectionFactoryInterface::class), + $this->app->make(PropertyMetadataFactoryInterface::class), + new AlternateUriResourceMetadataCollectionFactory( + new FiltersResourceMetadataCollectionFactory( + new FormatsResourceMetadataCollectionFactory( + new InputOutputResourceMetadataCollectionFactory( + new PhpDocResourceMetadataCollectionFactory( + new OperationNameResourceMetadataCollectionFactory( + new LinkResourceMetadataCollectionFactory( + $app->make(LinkFactoryInterface::class), + new UriTemplateResourceMetadataCollectionFactory( + $app->make(LinkFactoryInterface::class), + $app->make(PathSegmentNameGeneratorInterface::class), + new NotExposedOperationResourceMetadataCollectionFactory( + $app->make(LinkFactoryInterface::class), + new AttributesResourceMetadataCollectionFactory( + new ConcernsResourceMetadataCollectionFactory( + null, + $app->make(LoggerInterface::class), + $config->get('api-platform.defaults', []), + $config->get('api-platform.graphql.enabled'), + ), + $app->make(LoggerInterface::class), + $config->get('api-platform.defaults', []), + $config->get('api-platform.graphql.enabled'), + ), + ) + ), + $config->get('api-platform.graphql.enabled') + ) + ) + ) + ), + $formats, + $config->get('api-platform.patch_formats'), + ) + ) + ), + $app->make('filters'), + $app->make(CamelCaseToSnakeCaseNameConverter::class), + $this->app->make(LoggerInterface::class) + ), + $app->make('filters') + ) + ), + true === $config->get('app.debug') ? 'array' : $config->get('api-platform.cache', 'file') + ); + }); + + $this->app->extend( + ExceptionHandler::class, + function (ExceptionHandler $decorated, Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new ErrorHandler( + $app, + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ApiPlatformController::class), + $app->make(IdentifiersExtractorInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(Negotiator::class), + $config->get('api-platform.exception_to_status'), + $config->get('app.debug'), + $config->get('api-platform.error_formats'), + $decorated + ); + } + ); + + if (interface_exists(FieldsBuilderEnumInterface::class)) { + $this->registerGraphQl(); + } + } + + private function registerGraphQl(): void + { + $this->app->singleton('api_platform.graphql.state_provider.parameter', function (Application $app) { + $tagged = iterator_to_array($app->tagged(ParameterProviderInterface::class)); + $tagged['api_platform.serializer.filter_parameter_provider'] = $app->make(SerializerFilterParameterProvider::class); + + return new ParameterProvider( + new ParameterValidatorProvider( + new SecurityParameterProvider( + $app->make(GraphQlDenormalizeProvider::class), + $app->make(ResourceAccessCheckerInterface::class) + ), + ), + new ServiceLocator($tagged) + ); + }); + + $this->app->singleton(FieldsBuilderEnumInterface::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new FieldsBuilder( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(TypesContainerInterface::class), + $app->make(ContextAwareTypeBuilderInterface::class), + $app->make(TypeConverterInterface::class), + $app->make(ResolverFactoryInterface::class), + $app->make('filters'), + $app->make(Pagination::class), + $app->make(NameConverterInterface::class), + $config->get('api-platform.graphql.nesting_separator') ?? '__', + $app->make(InflectorInterface::class) + ); + }); + } + + /** + * @param array $classes + * @param class-string $interface + * @param array $apiPlatformProviders + */ + private function autoconfigure(array $classes, string $interface, array $apiPlatformProviders): void + { + $m = $apiPlatformProviders; + foreach ($classes as $className => $refl) { + if ($refl->implementsInterface($interface)) { + $m[] = $className; + } + } + + if ($m) { + $this->app->tag($m, $interface); + } + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides(): array + { + return [ + CallableProvider::class, + CallableProcessor::class, + ItemProvider::class, + CollectionProvider::class, + SerializerFilterParameterProvider::class, + ParameterProvider::class, + FilterQueryExtension::class, + 'filters', + ResourceMetadataCollectionFactoryInterface::class, + 'api_platform.graphql.state_provider.parameter', + FieldsBuilderEnumInterface::class, + ExceptionHandlerInterface::class, + ]; + } +} diff --git a/src/Laravel/ApiPlatformMiddleware.php b/src/Laravel/ApiPlatformMiddleware.php new file mode 100644 index 00000000000..130b5cb417a --- /dev/null +++ b/src/Laravel/ApiPlatformMiddleware.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactory; +use Illuminate\Http\Request; +use Symfony\Component\HttpFoundation\Response; + +class ApiPlatformMiddleware +{ + public function __construct( + protected OperationMetadataFactory $operationMetadataFactory, + ) { + } + + /** + * @param \Closure(Request): (Response) $next + */ + public function handle(Request $request, \Closure $next, ?string $operationName = null): Response + { + $operation = null; + if ($operationName) { + $request->attributes->set('_api_operation', $operation = $this->operationMetadataFactory->create($operationName)); + } + + if (!($format = $request->route('_format')) && $operation instanceof HttpOperation && str_ends_with($operation->getUriTemplate(), '{._format}')) { + $matches = []; + if (preg_match('/\.[a-zA-Z]+$/', $request->getPathInfo(), $matches)) { + $format = $matches[0]; + } + } + + $request->attributes->set('_format', $format ? substr($format, 1, \strlen($format) - 1) : ''); + + return $next($request); + } +} diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php new file mode 100644 index 00000000000..2e8a8094cbe --- /dev/null +++ b/src/Laravel/ApiPlatformProvider.php @@ -0,0 +1,1224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel; + +use ApiPlatform\GraphQl\Error\ErrorHandler as GraphQlErrorHandler; +use ApiPlatform\GraphQl\Error\ErrorHandlerInterface; +use ApiPlatform\GraphQl\Executor; +use ApiPlatform\GraphQl\ExecutorInterface; +use ApiPlatform\GraphQl\Metadata\RuntimeOperationMetadataFactory; +use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactory; +use ApiPlatform\GraphQl\Resolver\Factory\ResolverFactoryInterface; +use ApiPlatform\GraphQl\Resolver\QueryCollectionResolverInterface; +use ApiPlatform\GraphQl\Resolver\QueryItemResolverInterface; +use ApiPlatform\GraphQl\Resolver\ResourceFieldResolver; +use ApiPlatform\GraphQl\Serializer\Exception\ErrorNormalizer as GraphQlErrorNormalizer; +use ApiPlatform\GraphQl\Serializer\Exception\HttpExceptionNormalizer as GraphQlHttpExceptionNormalizer; +use ApiPlatform\GraphQl\Serializer\Exception\RuntimeExceptionNormalizer as GraphQlRuntimeExceptionNormalizer; +use ApiPlatform\GraphQl\Serializer\Exception\ValidationExceptionNormalizer as GraphQlValidationExceptionNormalizer; +use ApiPlatform\GraphQl\Serializer\ItemNormalizer as GraphQlItemNormalizer; +use ApiPlatform\GraphQl\Serializer\ObjectNormalizer as GraphQlObjectNormalizer; +use ApiPlatform\GraphQl\Serializer\SerializerContextBuilder as GraphQlSerializerContextBuilder; +use ApiPlatform\GraphQl\State\Processor\NormalizeProcessor; +use ApiPlatform\GraphQl\State\Provider\DenormalizeProvider as GraphQlDenormalizeProvider; +use ApiPlatform\GraphQl\State\Provider\ReadProvider as GraphQlReadProvider; +use ApiPlatform\GraphQl\State\Provider\ResolverProvider; +use ApiPlatform\GraphQl\Type\ContextAwareTypeBuilderInterface; +use ApiPlatform\GraphQl\Type\FieldsBuilderEnumInterface; +use ApiPlatform\GraphQl\Type\SchemaBuilder; +use ApiPlatform\GraphQl\Type\SchemaBuilderInterface; +use ApiPlatform\GraphQl\Type\TypeBuilder; +use ApiPlatform\GraphQl\Type\TypeConverter; +use ApiPlatform\GraphQl\Type\TypeConverterInterface; +use ApiPlatform\GraphQl\Type\TypesContainer; +use ApiPlatform\GraphQl\Type\TypesContainerInterface; +use ApiPlatform\GraphQl\Type\TypesFactory; +use ApiPlatform\GraphQl\Type\TypesFactoryInterface; +use ApiPlatform\Hal\Serializer\CollectionNormalizer as HalCollectionNormalizer; +use ApiPlatform\Hal\Serializer\EntrypointNormalizer as HalEntrypointNormalizer; +use ApiPlatform\Hal\Serializer\ItemNormalizer as HalItemNormalizer; +use ApiPlatform\Hal\Serializer\ObjectNormalizer as HalObjectNormalizer; +use ApiPlatform\HttpCache\State\AddHeadersProcessor; +use ApiPlatform\Hydra\JsonSchema\SchemaFactory as HydraSchemaFactory; +use ApiPlatform\Hydra\Serializer\CollectionFiltersNormalizer as HydraCollectionFiltersNormalizer; +use ApiPlatform\Hydra\Serializer\CollectionNormalizer as HydraCollectionNormalizer; +use ApiPlatform\Hydra\Serializer\DocumentationNormalizer as HydraDocumentationNormalizer; +use ApiPlatform\Hydra\Serializer\EntrypointNormalizer as HydraEntrypointNormalizer; +use ApiPlatform\Hydra\Serializer\HydraPrefixNameConverter; +use ApiPlatform\Hydra\Serializer\PartialCollectionViewNormalizer as HydraPartialCollectionViewNormalizer; +use ApiPlatform\Hydra\State\HydraLinkProcessor; +use ApiPlatform\JsonApi\JsonSchema\SchemaFactory as JsonApiSchemaFactory; +use ApiPlatform\JsonApi\Serializer\CollectionNormalizer as JsonApiCollectionNormalizer; +use ApiPlatform\JsonApi\Serializer\EntrypointNormalizer as JsonApiEntrypointNormalizer; +use ApiPlatform\JsonApi\Serializer\ErrorNormalizer as JsonApiErrorNormalizer; +use ApiPlatform\JsonApi\Serializer\ItemNormalizer as JsonApiItemNormalizer; +use ApiPlatform\JsonApi\Serializer\ObjectNormalizer as JsonApiObjectNormalizer; +use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter; +use ApiPlatform\JsonLd\AnonymousContextBuilderInterface; +use ApiPlatform\JsonLd\ContextBuilder as JsonLdContextBuilder; +use ApiPlatform\JsonLd\ContextBuilderInterface; +use ApiPlatform\JsonLd\Serializer\ItemNormalizer as JsonLdItemNormalizer; +use ApiPlatform\JsonLd\Serializer\ObjectNormalizer as JsonLdObjectNormalizer; +use ApiPlatform\JsonSchema\DefinitionNameFactory; +use ApiPlatform\JsonSchema\DefinitionNameFactoryInterface; +use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory; +use ApiPlatform\JsonSchema\SchemaFactory; +use ApiPlatform\JsonSchema\SchemaFactoryInterface; +use ApiPlatform\Laravel\ApiResource\Error; +use ApiPlatform\Laravel\ApiResource\ValidationError; +use ApiPlatform\Laravel\Controller\DocumentationController; +use ApiPlatform\Laravel\Controller\EntrypointController; +use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilterParameterProvider; +use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentAttributePropertyMetadataFactory; +use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyMetadataFactory; +use ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property\EloquentPropertyNameCollectionMetadataFactory; +use ApiPlatform\Laravel\Eloquent\Metadata\IdentifiersExtractor as EloquentIdentifiersExtractor; +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use ApiPlatform\Laravel\Eloquent\Metadata\ResourceClassResolver as EloquentResourceClassResolver; +use ApiPlatform\Laravel\Eloquent\PropertyAccess\PropertyAccessor as EloquentPropertyAccessor; +use ApiPlatform\Laravel\Eloquent\PropertyInfo\EloquentExtractor; +use ApiPlatform\Laravel\Eloquent\Serializer\EloquentNameConverter; +use ApiPlatform\Laravel\Eloquent\Serializer\SerializerContextBuilder as EloquentSerializerContextBuilder; +use ApiPlatform\Laravel\GraphQl\Controller\EntrypointController as GraphQlEntrypointController; +use ApiPlatform\Laravel\GraphQl\Controller\GraphiQlController; +use ApiPlatform\Laravel\JsonApi\State\JsonApiProvider; +use ApiPlatform\Laravel\Metadata\CachePropertyMetadataFactory; +use ApiPlatform\Laravel\Metadata\CachePropertyNameCollectionMetadataFactory; +use ApiPlatform\Laravel\Routing\IriConverter; +use ApiPlatform\Laravel\Routing\Router as UrlGeneratorRouter; +use ApiPlatform\Laravel\Routing\SkolemIriConverter; +use ApiPlatform\Laravel\Security\ResourceAccessChecker; +use ApiPlatform\Laravel\State\AccessCheckerProvider; +use ApiPlatform\Laravel\State\SwaggerUiProcessor; +use ApiPlatform\Laravel\State\SwaggerUiProvider; +use ApiPlatform\Laravel\State\ValidateProvider; +use ApiPlatform\Metadata\IdentifiersExtractor; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; +use ApiPlatform\Metadata\InflectorInterface; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactory; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; +use ApiPlatform\Metadata\Operation\PathSegmentNameGeneratorInterface; +use ApiPlatform\Metadata\Operation\UnderscorePathSegmentNameGenerator; +use ApiPlatform\Metadata\Property\Factory\AttributePropertyMetadataFactory; +use ApiPlatform\Metadata\Property\Factory\ClassLevelAttributePropertyNameCollectionFactory; +use ApiPlatform\Metadata\Property\Factory\ConcernsPropertyNameCollectionMetadataFactory; +use ApiPlatform\Metadata\Property\Factory\PropertyInfoPropertyMetadataFactory; +use ApiPlatform\Metadata\Property\Factory\PropertyInfoPropertyNameCollectionFactory; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\SerializerPropertyMetadataFactory; +use ApiPlatform\Metadata\Resource\Factory\AttributesResourceNameCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\ConcernsResourceNameCollectionFactory; +use ApiPlatform\Metadata\Resource\Factory\LinkFactory; +use ApiPlatform\Metadata\Resource\Factory\LinkFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; +use ApiPlatform\Metadata\ResourceClassResolver; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Metadata\Util\Inflector; +use ApiPlatform\OpenApi\Command\OpenApiCommand; +use ApiPlatform\OpenApi\Factory\OpenApiFactory; +use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; +use ApiPlatform\OpenApi\Options; +use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer; +use ApiPlatform\Serializer\ItemNormalizer; +use ApiPlatform\Serializer\JsonEncoder; +use ApiPlatform\Serializer\Mapping\Factory\ClassMetadataFactory as SerializerClassMetadataFactory; +use ApiPlatform\Serializer\Mapping\Loader\PropertyMetadataLoader; +use ApiPlatform\Serializer\SerializerContextBuilder; +use ApiPlatform\State\CallableProcessor; +use ApiPlatform\State\CallableProvider; +use ApiPlatform\State\ErrorProvider; +use ApiPlatform\State\Pagination\Pagination; +use ApiPlatform\State\Pagination\PaginationOptions; +use ApiPlatform\State\Processor\AddLinkHeaderProcessor; +use ApiPlatform\State\Processor\RespondProcessor; +use ApiPlatform\State\Processor\SerializeProcessor; +use ApiPlatform\State\Processor\WriteProcessor; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\Provider\ContentNegotiationProvider; +use ApiPlatform\State\Provider\DeserializeProvider; +use ApiPlatform\State\Provider\ParameterProvider; +use ApiPlatform\State\Provider\ReadProvider; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; +use Illuminate\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Routing\Router; +use Illuminate\Support\ServiceProvider; +use Negotiation\Negotiator; +use PHPStan\PhpDocParser\Parser\PhpDocParser; +use Psr\Log\LoggerInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractor; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Serializer\Encoder\CsvEncoder; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; +use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; +use Symfony\Component\Serializer\Mapping\Loader\LoaderInterface; +use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\BackedEnumNormalizer; +use Symfony\Component\Serializer\Normalizer\DateIntervalNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeZoneNormalizer; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\WebLink\HttpHeaderSerializer; + +class ApiPlatformProvider extends ServiceProvider +{ + /** + * Register services. + */ + public function register(): void + { + $this->mergeConfigFrom(__DIR__.'/config/api-platform.php', 'api-platform'); + + $this->app->singleton(PropertyInfoExtractorInterface::class, function (Application $app) { + $phpstanExtractor = class_exists(PhpDocParser::class) ? new PhpStanExtractor() : null; + $reflectionExtractor = new ReflectionExtractor(); + $eloquentExtractor = new EloquentExtractor($app->make(ModelMetadata::class)); + + return new PropertyInfoExtractor( + [$reflectionExtractor], + $phpstanExtractor ? [$phpstanExtractor, $reflectionExtractor] : [$reflectionExtractor], + [], + [$eloquentExtractor], + [$reflectionExtractor] + ); + }); + + $this->app->singleton(ModelMetadata::class, function () { + return new ModelMetadata(); + }); + + $this->app->bind(LoaderInterface::class, AttributeLoader::class); + $this->app->bind(ClassMetadataFactoryInterface::class, ClassMetadataFactory::class); + $this->app->singleton(ClassMetadataFactory::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $nameConverter = $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class); + if ($nameConverter && class_exists($nameConverter)) { + $nameConverter = new EloquentNameConverter($app->make($nameConverter)); + } + + return new ClassMetadataFactory( + new LoaderChain([ + new PropertyMetadataLoader( + $app->make(PropertyNameCollectionFactoryInterface::class), + $nameConverter + ), + new AttributeLoader(), + // new RelationMetadataLoader($app->make(ModelMetadata::class)), + ]) + ); + }); + + $this->app->singleton(SerializerClassMetadataFactory::class, function (Application $app) { + return new SerializerClassMetadataFactory($app->make(ClassMetadataFactoryInterface::class)); + }); + + $this->app->bind(PathSegmentNameGeneratorInterface::class, UnderscorePathSegmentNameGenerator::class); + + $this->app->singleton(ResourceNameCollectionFactoryInterface::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $paths = $config->get('api-platform.resources') ?? []; + $refl = new \ReflectionClass(Error::class); + $paths[] = \dirname($refl->getFileName()); + + $logger = $app->make(LoggerInterface::class); + + foreach ($paths as $i => $path) { + if (!file_exists($path)) { + $logger->warning(\sprintf('We skipped reading resources in "%s" as the path does not exist. Please check the configuration at "api-platform.resources".', $path)); + unset($paths[$i]); + } + } + + return new ConcernsResourceNameCollectionFactory($paths, new AttributesResourceNameCollectionFactory($paths)); + }); + + $this->app->bind(ResourceClassResolverInterface::class, ResourceClassResolver::class); + $this->app->singleton(ResourceClassResolver::class, function (Application $app) { + return new EloquentResourceClassResolver(new ResourceClassResolver($app->make(ResourceNameCollectionFactoryInterface::class))); + }); + + $this->app->singleton(PropertyMetadataFactoryInterface::class, function (Application $app) { + /** @var ConfigRepository $config */ + $config = $app['config']; + $nameConverter = $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class); + if ($nameConverter && class_exists($nameConverter)) { + $nameConverter = new EloquentNameConverter($app->make($nameConverter)); + } + + return new CachePropertyMetadataFactory( + new SchemaPropertyMetadataFactory( + $app->make(ResourceClassResolverInterface::class), + new SerializerPropertyMetadataFactory( + $app->make(SerializerClassMetadataFactory::class), + new PropertyInfoPropertyMetadataFactory( + $app->make(PropertyInfoExtractorInterface::class), + new AttributePropertyMetadataFactory( + new EloquentAttributePropertyMetadataFactory( + new EloquentPropertyMetadataFactory( + $app->make(ModelMetadata::class), + ), + ), + $nameConverter + ), + $app->make(ResourceClassResolverInterface::class) + ), + ) + ), + true === $config->get('app.debug') ? 'array' : $config->get('api-platform.cache', 'file') + ); + }); + + $this->app->singleton(PropertyNameCollectionFactoryInterface::class, function (Application $app) { + /** @var ConfigRepository $config */ + $config = $app['config']; + $nameConverter = $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class); + if ($nameConverter && class_exists($nameConverter)) { + $nameConverter = new EloquentNameConverter($app->make($nameConverter)); + } + + return new CachePropertyNameCollectionMetadataFactory( + new ClassLevelAttributePropertyNameCollectionFactory( + new ConcernsPropertyNameCollectionMetadataFactory( + new EloquentPropertyNameCollectionMetadataFactory( + $app->make(ModelMetadata::class), + new PropertyInfoPropertyNameCollectionFactory($app->make(PropertyInfoExtractorInterface::class)), + $app->make(ResourceClassResolverInterface::class) + ) + ), + $nameConverter + ), + true === $config->get('app.debug') ? 'array' : $config->get('api-platform.cache', 'file') + ); + }); + + $this->app->singleton(LinkFactoryInterface::class, function (Application $app) { + return new LinkFactory( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + ); + }); + + $this->app->bind(PropertyAccessorInterface::class, function () { + return new EloquentPropertyAccessor(); + }); + + $this->app->bind(NameConverterInterface::class, function (Application $app) { + $config = $app['config']; + $nameConverter = $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class); + if ($nameConverter && class_exists($nameConverter)) { + $nameConverter = new EloquentNameConverter($app->make($nameConverter)); + } + + $defaultContext = $config->get('api-platform.serializer', []); + + return new HydraPrefixNameConverter(new MetadataAwareNameConverter($app->make(ClassMetadataFactoryInterface::class), $nameConverter), $defaultContext); + }); + + $this->app->singleton(OperationMetadataFactory::class, function (Application $app) { + return new OperationMetadataFactory($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ResourceMetadataCollectionFactoryInterface::class)); + }); + + $this->app->bind(OperationMetadataFactoryInterface::class, OperationMetadataFactory::class); + + $this->app->singleton(ReadProvider::class, function (Application $app) { + return new ReadProvider($app->make(CallableProvider::class)); + }); + + $this->app->singleton(SwaggerUiProvider::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new SwaggerUiProvider($app->make(ReadProvider::class), $app->make(OpenApiFactoryInterface::class), $config->get('api-platform.swagger_ui.enabled', false)); + }); + + $this->app->singleton(DeserializeProvider::class, function (Application $app) { + return new DeserializeProvider($app->make(SwaggerUiProvider::class), $app->make(SerializerInterface::class), $app->make(SerializerContextBuilderInterface::class)); + }); + + $this->app->singleton(ValidateProvider::class, function (Application $app) { + $config = $app['config']; + $nameConverter = $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class); + if ($nameConverter && class_exists($nameConverter)) { + $nameConverter = $app->make($nameConverter); + } + + return new ValidateProvider($app->make(DeserializeProvider::class), $app, $app->make(ObjectNormalizer::class), $nameConverter); + }); + + if (class_exists(JsonApiProvider::class)) { + $this->app->extend(DeserializeProvider::class, function (ProviderInterface $inner, Application $app) { + return new JsonApiProvider($inner); + }); + } + + $this->app->singleton(SortFilterParameterProvider::class, function (Application $app) { + return new SortFilterParameterProvider(); + }); + + $this->app->singleton(AccessCheckerProvider::class, function (Application $app) { + return new AccessCheckerProvider($app->make(ParameterProvider::class), $app->make(ResourceAccessCheckerInterface::class)); + }); + + $this->app->singleton(Negotiator::class, function (Application $app) { + return new Negotiator(); + }); + $this->app->singleton(ContentNegotiationProvider::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new ContentNegotiationProvider($app->make(AccessCheckerProvider::class), $app->make(Negotiator::class), $config->get('api-platform.formats'), $config->get('api-platform.error_formats')); + }); + + $this->app->bind(ProviderInterface::class, ContentNegotiationProvider::class); + + $this->app->singleton(RespondProcessor::class, function (Application $app) { + $decorated = new RespondProcessor(); + if (class_exists(AddHeadersProcessor::class)) { + /** @var ConfigRepository */ + $config = $app['config']->get('api-platform.http_cache') ?? []; + + $decorated = new AddHeadersProcessor( + $decorated, + etag: $config['etag'] ?? false, + maxAge: $config['max_age'] ?? null, + sharedMaxAge: $config['shared_max_age'] ?? null, + vary: $config['vary'] ?? null, + public: $config['public'] ?? null, + staleWhileRevalidate: $config['stale_while_revalidate'] ?? null, + staleIfError: $config['stale_if_error'] ?? null + ); + } + + return new AddLinkHeaderProcessor($decorated, new HttpHeaderSerializer()); + }); + + $this->app->singleton(SerializeProcessor::class, function (Application $app) { + return new SerializeProcessor($app->make(RespondProcessor::class), $app->make(Serializer::class), $app->make(SerializerContextBuilderInterface::class)); + }); + + $this->app->singleton(WriteProcessor::class, function (Application $app) { + return new WriteProcessor($app->make(SerializeProcessor::class), $app->make(CallableProcessor::class)); + }); + + $this->app->singleton(SerializerContextBuilder::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new SerializerContextBuilder($app->make(ResourceMetadataCollectionFactoryInterface::class), $config->get('app.debug')); + }); + $this->app->bind(SerializerContextBuilderInterface::class, EloquentSerializerContextBuilder::class); + $this->app->singleton(EloquentSerializerContextBuilder::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new EloquentSerializerContextBuilder( + $app->make(SerializerContextBuilder::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $config->get('api-platform.name_converter', SnakeCaseToCamelCaseNameConverter::class) + ); + }); + + $this->app->singleton(HydraLinkProcessor::class, function (Application $app) { + return new HydraLinkProcessor($app->make(WriteProcessor::class), $app->make(UrlGeneratorInterface::class)); + }); + + $this->app->bind(ProcessorInterface::class, function (Application $app) { + $config = $app['config']; + if ($config->has('api-platform.formats.jsonld')) { + return $app->make(HydraLinkProcessor::class); + } + + return $app->make(WriteProcessor::class); + }); + + $this->app->singleton(ObjectNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new ObjectNormalizer(defaultContext: $defaultContext); + }); + + $this->app->singleton(DateTimeNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new DateTimeNormalizer(defaultContext: $defaultContext); + }); + + $this->app->singleton(DateTimeZoneNormalizer::class, function () { + return new DateTimeZoneNormalizer(); + }); + + $this->app->singleton(DateIntervalNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new DateIntervalNormalizer(defaultContext: $defaultContext); + }); + + $this->app->singleton(JsonEncoder::class, function () { + return new JsonEncoder('jsonld'); + }); + + $this->app->bind(IriConverterInterface::class, IriConverter::class); + $this->app->singleton(IriConverter::class, function (Application $app) { + return new IriConverter($app->make(CallableProvider::class), $app->make(OperationMetadataFactoryInterface::class), $app->make(UrlGeneratorRouter::class), $app->make(IdentifiersExtractorInterface::class), $app->make(ResourceClassResolverInterface::class), $app->make(ResourceMetadataCollectionFactoryInterface::class), $app->make(SkolemIriConverter::class)); + }); + + $this->app->singleton(SkolemIriConverter::class, function (Application $app) { + return new SkolemIriConverter($app->make(UrlGeneratorRouter::class)); + }); + + $this->app->bind(IdentifiersExtractorInterface::class, IdentifiersExtractor::class); + $this->app->singleton(IdentifiersExtractor::class, function (Application $app) { + return new EloquentIdentifiersExtractor( + new IdentifiersExtractor( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(PropertyAccessorInterface::class) + ) + ); + }); + + $this->app->bind(UrlGeneratorInterface::class, UrlGeneratorRouter::class); + $this->app->singleton(UrlGeneratorRouter::class, function (Application $app) { + $request = $app->make('request'); + // https://github.com/laravel/framework/blob/2bfb70bca53e24227a6f921f39d84ba452efd8e0/src/Illuminate/Routing/CompiledRouteCollection.php#L112 + $trimmedRequest = $request->duplicate(); + $parts = explode('?', $request->server->get('REQUEST_URI'), 2); + $trimmedRequest->server->set( + 'REQUEST_URI', + rtrim($parts[0], '/').(isset($parts[1]) ? '?'.$parts[1] : '') + ); + + $urlGenerator = new UrlGeneratorRouter($app->make(Router::class)); + $urlGenerator->setContext((new RequestContext())->fromRequest($trimmedRequest)); + + return $urlGenerator; + }); + + $this->app->bind(ContextBuilderInterface::class, JsonLdContextBuilder::class); + $this->app->singleton(JsonLdContextBuilder::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new JsonLdContextBuilder( + $app->make(ResourceNameCollectionFactoryInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(UrlGeneratorInterface::class), + $app->make(IriConverterInterface::class), + $app->make(NameConverterInterface::class), + $defaultContext + ); + }); + + $this->app->singleton(HydraEntrypointNormalizer::class, function (Application $app) { + return new HydraEntrypointNormalizer($app->make(ResourceMetadataCollectionFactoryInterface::class), $app->make(IriConverterInterface::class), $app->make(UrlGeneratorInterface::class)); + }); + + $this->app->singleton(ResourceAccessCheckerInterface::class, function () { + return new ResourceAccessChecker(); + }); + + $this->app->singleton(ItemNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new ItemNormalizer( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ClassMetadataFactoryInterface::class), + $app->make(LoggerInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceAccessCheckerInterface::class), + $defaultContext, + // $app->make(TagCollectorInterface::class) + ); + }); + + $this->app->bind(AnonymousContextBuilderInterface::class, JsonLdContextBuilder::class); + + $this->app->singleton(JsonLdObjectNormalizer::class, function (Application $app) { + return new JsonLdObjectNormalizer( + $app->make(ObjectNormalizer::class), + $app->make(IriConverterInterface::class), + $app->make(AnonymousContextBuilderInterface::class) + ); + }); + + $this->app->singleton(HalCollectionNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new HalCollectionNormalizer( + $app->make(ResourceClassResolverInterface::class), + $config->get('api-platform.pagination.page_parameter_name'), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + ); + }); + + $this->app->singleton(HalObjectNormalizer::class, function (Application $app) { + return new HalObjectNormalizer( + $app->make(ObjectNormalizer::class), + $app->make(IriConverterInterface::class) + ); + }); + + $this->app->singleton(HalItemNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new HalItemNormalizer( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ClassMetadataFactoryInterface::class), + $defaultContext, + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceAccessCheckerInterface::class), + // $app->make(TagCollectorInterface::class), + ); + }); + + $this->app->singleton(Options::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new Options( + title: $config->get('api-platform.title', ''), + description: $config->get('api-platform.description', ''), + version: $config->get('api-platform.version', ''), + oAuthEnabled: $config->get('api-platform.swagger_ui.oauth.enabled', false), + oAuthType: $config->get('api-platform.swagger_ui.oauth.type', null), + oAuthFlow: $config->get('api-platform.swagger_ui.oauth.flow', null), + oAuthTokenUrl: $config->get('api-platform.swagger_ui.oauth.tokenUrl', null), + oAuthAuthorizationUrl: $config->get('api-platform.swagger_ui.oauth.authorizationUrl', null), + oAuthRefreshUrl: $config->get('api-platform.swagger_ui.oauth.refreshUrl', null), + oAuthScopes: $config->get('api-platform.swagger_ui.oauth.scopes', []), + apiKeys: $config->get('api-platform.swagger_ui.apiKeys', []), + contactName: $config->get('api-platform.swagger_ui.contact.name', ''), + contactUrl: $config->get('api-platform.swagger_ui.contact.url', ''), + contactEmail: $config->get('api-platform.swagger_ui.contact.email', ''), + licenseName: $config->get('api-platform.swagger_ui.license.name', ''), + licenseUrl: $config->get('api-platform.swagger_ui.license.url', ''), + persistAuthorization: $config->get('api-platform.swagger_ui.persist_authorization', false), + httpAuth: $config->get('api-platform.swagger_ui.http_auth', []), + tags: $config->get('api-platform.openapi.tags', []), + errorResourceClass: Error::class, + validationErrorResourceClass: ValidationError::class + ); + }); + + $this->app->singleton(SwaggerUiProcessor::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new SwaggerUiProcessor( + urlGenerator: $app->make(UrlGeneratorInterface::class), + normalizer: $app->make(NormalizerInterface::class), + openApiOptions: $app->make(Options::class), + oauthClientId: $config->get('api-platform.swagger_ui.oauth.clientId'), + oauthClientSecret: $config->get('api-platform.swagger_ui.oauth.clientSecret'), + oauthPkce: $config->get('api-platform.swagger_ui.oauth.pkce', false), + ); + }); + + $this->app->singleton(DocumentationController::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new DocumentationController($app->make(ResourceNameCollectionFactoryInterface::class), $config->get('api-platform.title') ?? '', $config->get('api-platform.description') ?? '', $config->get('api-platform.version') ?? '', $app->make(OpenApiFactoryInterface::class), $app->make(ProviderInterface::class), $app->make(ProcessorInterface::class), $app->make(Negotiator::class), $config->get('api-platform.docs_formats'), $config->get('api-platform.swagger_ui.enabled', false)); + }); + + $this->app->singleton(EntrypointController::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new EntrypointController($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ProviderInterface::class), $app->make(ProcessorInterface::class), $config->get('api-platform.docs_formats')); + }); + + $this->app->singleton(Pagination::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new Pagination($config->get('api-platform.pagination'), []); + }); + + $this->app->singleton(PaginationOptions::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $defaults = $config->get('api-platform.defaults'); + $pagination = $config->get('api-platform.pagination'); + + return new PaginationOptions( + $defaults['pagination_enabled'] ?? true, + $pagination['page_parameter_name'] ?? 'page', + $defaults['pagination_client_items_per_page'] ?? false, + $pagination['items_per_page_parameter_name'] ?? 'itemsPerPage', + $defaults['pagination_client_enabled'] ?? false, + $pagination['enabled_parameter_name'] ?? 'pagination', + $defaults['pagination_items_per_page'] ?? 30, + $defaults['pagination_maximum_items_per_page'] ?? 30, + $defaults['pagination_partial'] ?? false, + $defaults['pagination_client_partial'] ?? false, + $pagination['partial_parameter_name'] ?? 'partial', + ); + }); + + $this->app->bind(OpenApiFactoryInterface::class, OpenApiFactory::class); + $this->app->singleton(OpenApiFactory::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new OpenApiFactory( + $app->make(ResourceNameCollectionFactoryInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(SchemaFactoryInterface::class), + null, + $config->get('api-platform.formats'), + $app->make(Options::class), + $app->make(PaginationOptions::class), + null, + $config->get('api-platform.error_formats'), + // ?RouterInterface $router = null + ); + }); + + $this->app->singleton(OpenApiCommand::class, function (Application $app) { + return new OpenApiCommand($app->make(OpenApiFactory::class), $app->make(Serializer::class)); + }); + + $this->app->bind(DefinitionNameFactoryInterface::class, DefinitionNameFactory::class); + $this->app->singleton(DefinitionNameFactory::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new DefinitionNameFactory($config->get('api-platform.formats')); + }); + + $this->app->singleton(SchemaFactory::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new SchemaFactory( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $config->get('api-platform.formats'), + $app->make(DefinitionNameFactoryInterface::class), + ); + }); + $this->app->singleton(JsonApiSchemaFactory::class, function (Application $app) { + return new JsonApiSchemaFactory( + $app->make(SchemaFactory::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(DefinitionNameFactoryInterface::class), + ); + }); + $this->app->singleton(HydraSchemaFactory::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new HydraSchemaFactory( + $app->make(JsonApiSchemaFactory::class), + $defaultContext + ); + }); + + $this->app->bind(SchemaFactoryInterface::class, HydraSchemaFactory::class); + + $this->app->singleton(OpenApiNormalizer::class, function (Application $app) { + return new OpenApiNormalizer($app->make(ObjectNormalizer::class)); + }); + + $this->app->singleton(HydraDocumentationNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + $entrypointEnabled = $config->get('api-platform.enable_entrypoint', true); + + return new HydraDocumentationNormalizer( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(UrlGeneratorInterface::class), + $app->make(NameConverterInterface::class), + $defaultContext, + $entrypointEnabled + ); + }); + + $this->app->singleton(HydraPartialCollectionViewNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new HydraPartialCollectionViewNormalizer( + new HydraCollectionFiltersNormalizer( + new HydraCollectionNormalizer( + $app->make(ContextBuilderInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(IriConverterInterface::class), + $defaultContext + ), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceClassResolverInterface::class), + null, // filterLocator, we use only Parameters with Laravel and we don't need to call filters there + $defaultContext + ), + 'page', + 'pagination', + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyAccessorInterface::class), + $config->get('api-platform.url_generation_strategy', UrlGeneratorInterface::ABS_PATH), + $defaultContext, + ); + }); + + $this->app->singleton(ReservedAttributeNameConverter::class, function (Application $app) { + return new ReservedAttributeNameConverter($app->make(NameConverterInterface::class)); + }); + + if (interface_exists(FieldsBuilderEnumInterface::class)) { + $this->registerGraphQl(); + } + + $this->app->singleton(JsonApiEntrypointNormalizer::class, function (Application $app) { + return new JsonApiEntrypointNormalizer( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(UrlGeneratorInterface::class), + ); + }); + + $this->app->singleton(JsonApiCollectionNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new JsonApiCollectionNormalizer( + $app->make(ResourceClassResolverInterface::class), + $config->get('api-platform.pagination.page_parameter_name'), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + ); + }); + + $this->app->singleton(JsonApiItemNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new JsonApiItemNormalizer( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ClassMetadataFactoryInterface::class), + $defaultContext, + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceAccessCheckerInterface::class), + // $app->make(TagCollectorInterface::class), + ); + }); + + $this->app->singleton(JsonApiErrorNormalizer::class, function (Application $app) { + return new JsonApiErrorNormalizer( + $app->make(JsonApiItemNormalizer::class), + ); + }); + + $this->app->singleton(JsonApiObjectNormalizer::class, function (Application $app) { + return new JsonApiObjectNormalizer( + $app->make(ObjectNormalizer::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + ); + }); + + $this->app->singleton('api_platform_normalizer_list', function (Application $app) { + $list = new \SplPriorityQueue(); + $list->insert($app->make(HydraEntrypointNormalizer::class), -800); + $list->insert($app->make(HydraPartialCollectionViewNormalizer::class), -800); + $list->insert($app->make(HalCollectionNormalizer::class), -800); + $list->insert($app->make(HalEntrypointNormalizer::class), -985); + $list->insert($app->make(HalObjectNormalizer::class), -995); + $list->insert($app->make(HalItemNormalizer::class), -890); + $list->insert($app->make(JsonLdItemNormalizer::class), -890); + $list->insert($app->make(JsonLdObjectNormalizer::class), -995); + $list->insert($app->make(ArrayDenormalizer::class), -990); + $list->insert($app->make(DateTimeZoneNormalizer::class), -915); + $list->insert($app->make(DateIntervalNormalizer::class), -915); + $list->insert($app->make(DateTimeNormalizer::class), -910); + $list->insert($app->make(BackedEnumNormalizer::class), -910); + $list->insert($app->make(ObjectNormalizer::class), -1000); + $list->insert($app->make(ItemNormalizer::class), -895); + $list->insert($app->make(OpenApiNormalizer::class), -780); + $list->insert($app->make(HydraDocumentationNormalizer::class), -790); + + $list->insert($app->make(JsonApiEntrypointNormalizer::class), -800); + $list->insert($app->make(JsonApiCollectionNormalizer::class), -985); + $list->insert($app->make(JsonApiItemNormalizer::class), -890); + $list->insert($app->make(JsonApiErrorNormalizer::class), -790); + $list->insert($app->make(JsonApiObjectNormalizer::class), -995); + + if (interface_exists(FieldsBuilderEnumInterface::class)) { + $list->insert($app->make(GraphQlItemNormalizer::class), -890); + $list->insert($app->make(GraphQlObjectNormalizer::class), -995); + $list->insert($app->make(GraphQlErrorNormalizer::class), -790); + $list->insert($app->make(GraphQlValidationExceptionNormalizer::class), -780); + $list->insert($app->make(GraphQlHttpExceptionNormalizer::class), -780); + $list->insert($app->make(GraphQlRuntimeExceptionNormalizer::class), -780); + } + + return $list; + }); + + $this->app->bind(SerializerInterface::class, Serializer::class); + $this->app->bind(NormalizerInterface::class, Serializer::class); + $this->app->singleton(Serializer::class, function (Application $app) { + // TODO: unused + implement hal/jsonapi ? + // $list->insert($dataUriNormalizer, -920); + // $list->insert($unwrappingDenormalizer, 1000); + // $list->insert($jsonserializableNormalizer, -900); + // $list->insert($uuidDenormalizer, -895); //Todo ramsey uuid support ? + + return new Serializer( + iterator_to_array($app->make('api_platform_normalizer_list')), + [ + new JsonEncoder('json'), + $app->make(JsonEncoder::class), + new JsonEncoder('jsonopenapi'), + new JsonEncoder('jsonapi'), + new JsonEncoder('jsonhal'), + new CsvEncoder(), + ] + ); + }); + + $this->app->singleton(JsonLdItemNormalizer::class, function (Application $app) { + $config = $app['config']; + $defaultContext = $config->get('api-platform.serializer', []); + + return new JsonLdItemNormalizer( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(ContextBuilderInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(ClassMetadataFactoryInterface::class), + $defaultContext, + $app->make(ResourceAccessCheckerInterface::class), + // $app->make(TagCollectorInterface::class) + ); + }); + + $this->app->singleton(InflectorInterface::class, function (Application $app) { + return new Inflector(); + }); + + if ($this->app->runningInConsole()) { + $this->commands([ + Console\InstallCommand::class, + Console\Maker\MakeStateProcessorCommand::class, + Console\Maker\MakeStateProviderCommand::class, + Console\Maker\MakeFilterCommand::class, + OpenApiCommand::class, + ]); + } + } + + private function registerGraphQl(): void + { + $this->app->singleton(GraphQlItemNormalizer::class, function (Application $app) { + return new GraphQlItemNormalizer( + $app->make(PropertyNameCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + $app->make(IriConverterInterface::class), + $app->make(IdentifiersExtractorInterface::class), + $app->make(ResourceClassResolverInterface::class), + $app->make(PropertyAccessorInterface::class), + $app->make(NameConverterInterface::class), + $app->make(SerializerClassMetadataFactory::class), + null, + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(ResourceAccessCheckerInterface::class) + ); + }); + + $this->app->singleton(GraphQlObjectNormalizer::class, function (Application $app) { + return new GraphQlObjectNormalizer( + $app->make(ObjectNormalizer::class), + $app->make(IriConverterInterface::class), + $app->make(IdentifiersExtractorInterface::class), + ); + }); + + $this->app->singleton(GraphQlErrorNormalizer::class, function () { + return new GraphQlErrorNormalizer(); + }); + + $this->app->singleton(GraphQlValidationExceptionNormalizer::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new GraphQlValidationExceptionNormalizer($config->get('api-platform.exception_to_status')); + }); + + $this->app->singleton(GraphQlHttpExceptionNormalizer::class, function () { + return new GraphQlHttpExceptionNormalizer(); + }); + + $this->app->singleton(GraphQlRuntimeExceptionNormalizer::class, function () { + return new GraphQlHttpExceptionNormalizer(); + }); + + $this->app->singleton('api_platform.graphql.type_locator', function (Application $app) { + $tagged = iterator_to_array($app->tagged('api_platform.graphql.type')); + $services = []; + foreach ($tagged as $service) { + $services[$service->name] = $service; + } + + return new ServiceLocator($services); + }); + + $this->app->singleton(TypesFactoryInterface::class, function (Application $app) { + $tagged = iterator_to_array($app->tagged('api_platform.graphql.type')); + + return new TypesFactory($app->make('api_platform.graphql.type_locator'), array_column($tagged, 'name')); + }); + + $this->app->singleton(TypesContainerInterface::class, function () { + return new TypesContainer(); + }); + + $this->app->singleton(ResourceFieldResolver::class, function (Application $app) { + return new ResourceFieldResolver($app->make(IriConverterInterface::class)); + }); + + $this->app->singleton(ContextAwareTypeBuilderInterface::class, function (Application $app) { + return new TypeBuilder( + $app->make(TypesContainerInterface::class), + $app->make(ResourceFieldResolver::class), + null, + $app->make(Pagination::class) + ); + }); + + $this->app->singleton(TypeConverterInterface::class, function (Application $app) { + return new TypeConverter( + $app->make(ContextAwareTypeBuilderInterface::class), + $app->make(TypesContainerInterface::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(PropertyMetadataFactoryInterface::class), + ); + }); + + $this->app->singleton(GraphQlSerializerContextBuilder::class, function (Application $app) { + return new GraphQlSerializerContextBuilder($app->make(NameConverterInterface::class)); + }); + + $this->app->singleton(GraphQlReadProvider::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new GraphQlReadProvider( + $this->app->make(CallableProvider::class), + $app->make(IriConverterInterface::class), + $app->make(GraphQlSerializerContextBuilder::class), + $config->get('api-platform.graphql.nesting_separator') ?? '__' + ); + }); + $this->app->alias(GraphQlReadProvider::class, 'api_platform.graphql.state_provider.read'); + + $this->app->singleton(ErrorProvider::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new ErrorProvider( + $config->get('app.debug'), + $app->make(ResourceClassResolver::class), + $app->make(ResourceMetadataCollectionFactoryInterface::class), + ); + }); + + $this->app->singleton(ResolverProvider::class, function (Application $app) { + $resolvers = iterator_to_array($app->tagged('api_platform.graphql.resolver')); + $taggedItemResolvers = iterator_to_array($app->tagged(QueryItemResolverInterface::class)); + $taggedCollectionResolvers = iterator_to_array($app->tagged(QueryCollectionResolverInterface::class)); + + return new ResolverProvider( + $app->make(GraphQlReadProvider::class), + new ServiceLocator([...$resolvers, ...$taggedItemResolvers, ...$taggedCollectionResolvers]), + ); + }); + + $this->app->alias(ResolverProvider::class, 'api_platform.graphql.state_provider.resolver'); + + $this->app->singleton(GraphQlDenormalizeProvider::class, function (Application $app) { + return new GraphQlDenormalizeProvider( + $this->app->make(ResolverProvider::class), + $app->make(SerializerInterface::class), + $app->make(GraphQlSerializerContextBuilder::class) + ); + }); + + $this->app->alias(GraphQlDenormalizeProvider::class, 'api_platform.graphql.state_provider.denormalize'); + + $this->app->singleton('api_platform.graphql.state_provider.access_checker', function (Application $app) { + return new AccessCheckerProvider($app->make('api_platform.graphql.state_provider.parameter'), $app->make(ResourceAccessCheckerInterface::class)); + }); + + $this->app->singleton(NormalizeProcessor::class, function (Application $app) { + return new NormalizeProcessor( + $app->make(SerializerInterface::class), + $app->make(GraphQlSerializerContextBuilder::class), + $app->make(Pagination::class) + ); + }); + $this->app->alias(NormalizeProcessor::class, 'api_platform.graphql.state_processor.normalize'); + + $this->app->singleton('api_platform.graphql.state_processor', function (Application $app) { + return new WriteProcessor( + $app->make('api_platform.graphql.state_processor.normalize'), + $app->make(CallableProcessor::class), + ); + }); + + $this->app->singleton(ResolverFactoryInterface::class, function (Application $app) { + return new ResolverFactory( + $app->make('api_platform.graphql.state_provider.access_checker'), + $app->make('api_platform.graphql.state_processor'), + $app->make('api_platform.graphql.runtime_operation_metadata_factory'), + ); + }); + + $this->app->singleton('api_platform.graphql.runtime_operation_metadata_factory', function (Application $app) { + return new RuntimeOperationMetadataFactory( + $app->make(ResourceMetadataCollectionFactoryInterface::class), + $app->make(UrlGeneratorRouter::class) + ); + }); + + $this->app->singleton(SchemaBuilderInterface::class, function (Application $app) { + return new SchemaBuilder($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ResourceMetadataCollectionFactoryInterface::class), $app->make(TypesFactoryInterface::class), $app->make(TypesContainerInterface::class), $app->make(FieldsBuilderEnumInterface::class)); + }); + + $this->app->singleton(ErrorHandlerInterface::class, function () { + return new GraphQlErrorHandler(); + }); + + $this->app->singleton(ExecutorInterface::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new Executor($config->get('api-platform.graphql.introspection.enabled') ?? false, $config->get('api-platform.graphql.max_query_complexity') ?? 500, $config->get('api-platform.graphql.max_query_depth') ?? 200); + }); + + $this->app->singleton(GraphiQlController::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + $prefix = $config->get('api-platform.defaults.route_prefix') ?? ''; + + return new GraphiQlController($prefix); + }); + + $this->app->singleton(GraphQlEntrypointController::class, function (Application $app) { + /** @var ConfigRepository */ + $config = $app['config']; + + return new GraphQlEntrypointController( + $app->make(SchemaBuilderInterface::class), + $app->make(ExecutorInterface::class), + $app->make(GraphiQlController::class), + $app->make(SerializerInterface::class), + $app->make(ErrorHandlerInterface::class), + debug: $config->get('app.debug'), + negotiator: $app->make(Negotiator::class), + formats: $config->get('api-platform.formats') + ); + }); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + if ($this->app->runningInConsole()) { + $this->publishes([ + __DIR__.'/config/api-platform.php' => $this->app->configPath('api-platform.php'), + ], 'api-platform-config'); + + $this->publishes([ + __DIR__.'/public' => $this->app->publicPath('vendor/api-platform'), + ], ['api-platform-assets', 'public']); + } + + $this->loadViewsFrom(__DIR__.'/resources/views', 'api-platform'); + + $config = $this->app['config']; + + if ($config->get('api-platform.graphql.enabled')) { + $fieldsBuilder = $this->app->make(FieldsBuilderEnumInterface::class); + $typeBuilder = $this->app->make(ContextAwareTypeBuilderInterface::class); + $typeBuilder->setFieldsBuilderLocator(new ServiceLocator(['api_platform.graphql.fields_builder' => $fieldsBuilder])); + } + + $this->loadRoutesFrom(__DIR__.'/routes/api.php'); + } +} diff --git a/src/Laravel/ApiResource/Error.php b/src/Laravel/ApiResource/Error.php new file mode 100644 index 00000000000..e013746f8fc --- /dev/null +++ b/src/Laravel/ApiResource/Error.php @@ -0,0 +1,210 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\ApiResource; + +use ApiPlatform\JsonSchema\SchemaFactory; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Error as Operation; +use ApiPlatform\Metadata\ErrorResource; +use ApiPlatform\Metadata\ErrorResourceInterface; +use ApiPlatform\Metadata\Exception\HttpExceptionInterface; +use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; +use ApiPlatform\State\ErrorProvider; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\Ignore; +use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\WebLink\Link; + +#[ErrorResource( + uriTemplate: '/errors/{status}{._format}', + openapi: false, + uriVariables: ['status'], + operations: [ + new Operation( + errors: [], + name: '_api_errors_problem', + routeName: '_api_errors', + outputFormats: ['json' => ['application/problem+json', 'application/json']], + hideHydraOperation: true, + normalizationContext: [ + SchemaFactory::OPENAPI_DEFINITION_NAME => '', + 'groups' => ['jsonproblem'], + 'skip_null_values' => true, + 'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'], + ], + ), + new Operation( + errors: [], + name: '_api_errors_hydra', + routeName: '_api_errors', + outputFormats: ['jsonld' => ['application/problem+json', 'application/ld+json']], + normalizationContext: [ + SchemaFactory::OPENAPI_DEFINITION_NAME => '', + 'groups' => ['jsonld'], + 'skip_null_values' => true, + 'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'], + ], + links: [new Link(rel: 'http://www.w3.org/ns/json-ld#error', href: 'http://www.w3.org/ns/hydra/error')], + ), + new Operation( + errors: [], + name: '_api_errors_jsonapi', + routeName: '_api_errors', + hideHydraOperation: true, + outputFormats: ['jsonapi' => ['application/vnd.api+json']], + normalizationContext: [ + SchemaFactory::OPENAPI_DEFINITION_NAME => '', + 'disable_json_schema_serializer_groups' => false, + 'groups' => ['jsonapi'], + 'skip_null_values' => true, + 'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'], + ], + ), + new Operation( + name: '_api_errors', + hideHydraOperation: true, + extraProperties: ['_api_disable_swagger_provider' => true], + outputFormats: ['html' => ['text/html'], 'jsonapi' => ['application/vnd.api+json'], 'jsonld' => ['application/ld+json'], 'json' => ['application/problem+json', 'application/json']], + ), + ], + outputFormats: ['jsonapi' => ['application/vnd.api+json'], 'jsonld' => ['application/ld+json'], 'json' => ['application/problem+json', 'application/json']], + provider: ErrorProvider::class, + graphQlOperations: [], + description: 'A representation of common errors.', +)] +#[ApiProperty(property: 'previous', hydra: false, readable: false)] +#[ApiProperty(property: 'traceAsString', hydra: false, readable: false)] +#[ApiProperty(property: 'string', hydra: false, readable: false)] +class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface, ErrorResourceInterface +{ + /** + * @var array + */ + private array $originalTrace; + + /** + * @param array $headers + * @param array $originalTrace + */ + public function __construct( + private readonly string $title, + private readonly string $detail, + #[ApiProperty(identifier: true)] private int $status, + array $originalTrace = [], + private readonly ?string $instance = null, + private string $type = 'about:blank', + private array $headers = [], + ) { + parent::__construct(); + + $this->originalTrace = []; + foreach ($originalTrace as $i => $t) { + unset($t['args']); // we don't want arguments in our JSON traces, especially with xdebug + $this->originalTrace[$i] = $t; + } + } + + /** + * @return array + */ + #[SerializedName('trace')] + #[Groups(['trace'])] + public function getOriginalTrace(): array + { + return $this->originalTrace; + } + + #[SerializedName('description')] + public function getDescription(): string + { + return $this->detail; + } + + public static function createFromException(\Exception|\Throwable $exception, int $status): self + { + $headers = ($exception instanceof SymfonyHttpExceptionInterface || $exception instanceof HttpExceptionInterface) ? $exception->getHeaders() : []; + + return new self('An error occurred', $exception->getMessage(), $status, $exception->getTrace(), type: '/errors/'.$status, headers: $headers); + } + + /** + * @return array + */ + #[Ignore] + public function getHeaders(): array + { + return $this->headers; + } + + #[Ignore] + public function getStatusCode(): int + { + return $this->status; + } + + #[Groups(['jsonapi'])] + public function getId(): string + { + return (string) $this->status; + } + + /** + * @param array $headers + */ + public function setHeaders(array $headers): void + { + $this->headers = $headers; + } + + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + public function getType(): string + { + return $this->type; + } + + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + public function getTitle(): ?string + { + return $this->title; + } + + public function setType(string $type): void + { + $this->type = $type; + } + + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + public function getStatus(): ?int + { + return $this->status; + } + + public function setStatus(int $status): void + { + $this->status = $status; + } + + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + public function getDetail(): ?string + { + return $this->detail; + } + + #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + public function getInstance(): ?string + { + return $this->instance; + } +} diff --git a/src/Laravel/ApiResource/ValidationError.php b/src/Laravel/ApiResource/ValidationError.php new file mode 100644 index 00000000000..aa2117f49ab --- /dev/null +++ b/src/Laravel/ApiResource/ValidationError.php @@ -0,0 +1,178 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\ApiResource; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Error as ErrorOperation; +use ApiPlatform\Metadata\ErrorResource; +use ApiPlatform\Metadata\Exception\HttpExceptionInterface; +use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; +use ApiPlatform\Metadata\Exception\RuntimeException; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Serializer\Annotation\SerializedName; +use Symfony\Component\WebLink\Link; + +/** + * Thrown when a validation error occurs. + * + * @author Kévin Dunglas + */ +#[ErrorResource( + uriTemplate: '/validation_errors/{id}', + status: 422, + openapi: false, + outputFormats: ['jsonapi' => ['application/vnd.api+json'], 'jsonld' => ['application/ld+json'], 'json' => ['application/problem+json', 'application/json']], + uriVariables: ['id'], + shortName: 'ValidationError', + operations: [ + new ErrorOperation( + routeName: 'api_validation_errors', + name: '_api_validation_errors_problem', + outputFormats: ['json' => ['application/problem+json']], + normalizationContext: [ + 'groups' => ['json'], + 'skip_null_values' => true, + 'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'], + ], + ), + new ErrorOperation( + name: '_api_validation_errors_hydra', + routeName: 'api_validation_errors', + outputFormats: ['jsonld' => ['application/problem+json']], + links: [new Link(rel: 'http://www.w3.org/ns/json-ld#error', href: 'http://www.w3.org/ns/hydra/error')], + normalizationContext: [ + 'groups' => ['jsonld'], + 'skip_null_values' => true, + 'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'], + ], + ), + new ErrorOperation( + name: '_api_validation_errors_jsonapi', + routeName: 'api_validation_errors', + outputFormats: ['jsonapi' => ['application/vnd.api+json']], + normalizationContext: [ + 'groups' => ['jsonapi'], + 'skip_null_values' => true, + 'ignored_attributes' => ['trace', 'file', 'line', 'code', 'message', 'traceAsString', 'previous'], + ], + ), + ], + graphQlOperations: [] +)] +class ValidationError extends RuntimeException implements \Stringable, ProblemExceptionInterface, HttpExceptionInterface, SymfonyHttpExceptionInterface +{ + private int $status = 422; + private string $id; + + /** + * @param array $violations + */ + public function __construct(string $message = '', mixed $code = null, ?\Throwable $previous = null, protected array $violations = []) + { + $this->id = (string) $code; + $this->setDetail($message); + parent::__construct($message ?: $this->__toString(), 422, $previous); + } + + public function getId(): string + { + return $this->id; + } + + #[SerializedName('description')] + #[Groups(['jsonld', 'json'])] + public function getDescription(): string + { + return $this->detail; + } + + #[Groups(['jsonld', 'json', 'jsonapi'])] + public function getType(): string + { + return '/validation_errors/'.$this->id; + } + + #[Groups(['jsonld', 'json', 'jsonapi'])] + public function getTitle(): ?string + { + return 'Validation Error'; + } + + #[Groups(['jsonld', 'json', 'jsonapi'])] + private string $detail; + + public function getDetail(): ?string + { + return $this->detail; + } + + public function setDetail(string $detail): void + { + $this->detail = $detail; + } + + #[Groups(['jsonld', 'json', 'jsonapi'])] + public function getStatus(): ?int + { + return $this->status; + } + + public function setStatus(int $status): void + { + $this->status = $status; + } + + #[Groups(['jsonld', 'json', 'jsonapi'])] + public function getInstance(): ?string + { + return null; + } + + /** + * @return array + */ + #[SerializedName('violations')] + #[Groups(['json', 'jsonld', 'jsonapi'])] + #[ApiProperty( + jsonldContext: ['@type' => 'ConstraintViolationList'], + schema: [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'propertyPath' => ['type' => 'string', 'description' => 'The property path of the violation'], + 'message' => ['type' => 'string', 'description' => 'The message associated with the violation'], + ], + ], + ] + )] + public function getViolations(): array + { + return $this->violations; + } + + public function getStatusCode(): int + { + return $this->status; + } + + /** + * @return array + */ + public function getHeaders(): array + { + return []; + } +} diff --git a/src/Laravel/CONTRIBUTING.md b/src/Laravel/CONTRIBUTING.md new file mode 100644 index 00000000000..7e6574ad2e4 --- /dev/null +++ b/src/Laravel/CONTRIBUTING.md @@ -0,0 +1,24 @@ +# Contributing to the Laravel Integration of API Platform + +Pull requests should be made at https://github.com/api-plaform/core + +## Tests + + cd src/Laravel + composer global require soyuka/pmu + composer global link ../../ + vendor/bin/testbench workbench:build + vendor/bin/testbench api-platform:install + vendor/bin/testbench package:test + # or + vendor/bin/phpunit + +A command is available to remove the database: + + vendor/bin/testbench workbench:drop-sqlite-db + +## Starting the Test App + +The test server is also available through: + + vendor/bin/testbench serve diff --git a/src/Laravel/Console/InstallCommand.php b/src/Laravel/Console/InstallCommand.php new file mode 100644 index 00000000000..ebd48482702 --- /dev/null +++ b/src/Laravel/Console/InstallCommand.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console; + +use Illuminate\Console\Command; +use Symfony\Component\Console\Attribute\AsCommand; + +#[AsCommand(name: 'api-platform:install')] +class InstallCommand extends Command +{ + /** + * @var string + */ + protected $signature = 'api-platform:install'; + + /** + * @var string + */ + protected $description = 'Install all of the API Platform resources'; + + /** + * Execute the console command. + */ + public function handle(): void + { + $this->comment('Publishing API Platform Assets...'); + $this->callSilent('vendor:publish', ['--tag' => 'api-platform-assets']); + + $this->comment('Publishing API Platform Configuration...'); + $this->callSilent('vendor:publish', ['--tag' => 'api-platform-config']); + + $this->info('API Platform installed successfully.'); + } +} diff --git a/src/Laravel/Console/Maker/AbstractMakeStateCommand.php b/src/Laravel/Console/Maker/AbstractMakeStateCommand.php new file mode 100644 index 00000000000..d1ed3f1ab4d --- /dev/null +++ b/src/Laravel/Console/Maker/AbstractMakeStateCommand.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker; + +use ApiPlatform\Laravel\Console\Maker\Utils\StateAppServiceProviderTagger; +use ApiPlatform\Laravel\Console\Maker\Utils\StateTemplateGenerator; +use ApiPlatform\Laravel\Console\Maker\Utils\StateTypeEnum; +use ApiPlatform\Laravel\Console\Maker\Utils\SuccessMessageTrait; +use Illuminate\Console\Command; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +abstract class AbstractMakeStateCommand extends Command +{ + use SuccessMessageTrait; + + public function __construct( + private readonly Filesystem $filesystem, + private readonly StateTemplateGenerator $stateTemplateGenerator, + private readonly StateAppServiceProviderTagger $stateAppServiceProviderTagger, + ) { + parent::__construct(); + } + + /** + * @throws FileNotFoundException + */ + public function handle(): int + { + $stateType = $this->getStateType()->name; + $stateName = $this->ask(\sprintf('Choose a class name for your state %s (e.g. AwesomeState%s)', strtolower($stateType), ucfirst($stateType))); + if (null === $stateName || '' === $stateName) { + $this->error('[ERROR] The name argument cannot be blank.'); + + return self::FAILURE; + } + + $directoryPath = base_path('app/State/'); + $this->filesystem->ensureDirectoryExists($directoryPath); + + $filePath = $this->stateTemplateGenerator->getFilePath($directoryPath, $stateName); + if ($this->filesystem->exists($filePath)) { + $this->error(\sprintf('[ERROR] The file "%s" can\'t be generated because it already exists.', $filePath)); + + return self::FAILURE; + } + + $this->stateTemplateGenerator->generate($filePath, $stateName, $this->getStateType()); + if (!$this->filesystem->exists($filePath)) { + $this->error(\sprintf('[ERROR] The file "%s" could not be created.', $filePath)); + + return self::FAILURE; + } + + $this->stateAppServiceProviderTagger->addTagToServiceProvider($stateName, $this->getStateType()); + + $this->writeSuccessMessage($filePath, \sprintf('State %s', ucfirst($this->getStateType()->name))); + + return self::SUCCESS; + } + + abstract protected function getStateType(): StateTypeEnum; +} diff --git a/src/Laravel/Console/Maker/MakeFilterCommand.php b/src/Laravel/Console/Maker/MakeFilterCommand.php new file mode 100644 index 00000000000..6d3756bf6c4 --- /dev/null +++ b/src/Laravel/Console/Maker/MakeFilterCommand.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker; + +use ApiPlatform\Laravel\Console\Maker\Utils\FilterAppServiceProviderTagger; +use ApiPlatform\Laravel\Console\Maker\Utils\FilterTemplateGenerator; +use ApiPlatform\Laravel\Console\Maker\Utils\SuccessMessageTrait; +use Illuminate\Console\Command; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +final class MakeFilterCommand extends Command +{ + use SuccessMessageTrait; + + protected $signature = 'make:filter'; + protected $description = 'Creates an API Platform filter'; + + public function __construct( + private readonly Filesystem $filesystem, + private readonly FilterTemplateGenerator $filterTemplateGenerator, + private readonly FilterAppServiceProviderTagger $filterAppServiceProviderTagger, + ) { + parent::__construct(); + } + + /** + * @throws FileNotFoundException + */ + public function handle(): int + { + $nameArgument = $this->ask('Choose a class name for your filter (e.g. AwesomeFilter)'); + if (null === $nameArgument || '' === $nameArgument) { + $this->error('[ERROR] The name argument cannot be blank.'); + + return self::FAILURE; + } + + $directoryPath = base_path('app/Filter/'); + $this->filesystem->ensureDirectoryExists($directoryPath); + + $filePath = $this->filterTemplateGenerator->getFilePath($directoryPath, $nameArgument); + if ($this->filesystem->exists($filePath)) { + $this->error(\sprintf('[ERROR] The file "%s" can\'t be generated because it already exists.', $filePath)); + + return self::FAILURE; + } + + $this->filterTemplateGenerator->generate($filePath, $nameArgument); + if (!$this->filesystem->exists($filePath)) { + $this->error(\sprintf('[ERROR] The file "%s" could not be created.', $filePath)); + + return self::FAILURE; + } + + $this->filterAppServiceProviderTagger->addTagToServiceProvider($nameArgument); + + $this->writeSuccessMessage($filePath, 'Eloquent Filter'); + + return self::SUCCESS; + } +} diff --git a/src/Laravel/Console/Maker/MakeStateProcessorCommand.php b/src/Laravel/Console/Maker/MakeStateProcessorCommand.php new file mode 100644 index 00000000000..960ae42582b --- /dev/null +++ b/src/Laravel/Console/Maker/MakeStateProcessorCommand.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker; + +use ApiPlatform\Laravel\Console\Maker\Utils\StateTypeEnum; + +final class MakeStateProcessorCommand extends AbstractMakeStateCommand +{ + protected $signature = 'make:state-processor'; + protected $description = 'Creates an API Platform state processor'; + + protected function getStateType(): StateTypeEnum + { + return StateTypeEnum::Processor; + } +} diff --git a/src/Laravel/Console/Maker/MakeStateProviderCommand.php b/src/Laravel/Console/Maker/MakeStateProviderCommand.php new file mode 100644 index 00000000000..ebb89b60327 --- /dev/null +++ b/src/Laravel/Console/Maker/MakeStateProviderCommand.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker; + +use ApiPlatform\Laravel\Console\Maker\Utils\StateTypeEnum; + +final class MakeStateProviderCommand extends AbstractMakeStateCommand +{ + protected $signature = 'make:state-provider'; + protected $description = 'Creates an API Platform state provider'; + + protected function getStateType(): StateTypeEnum + { + return StateTypeEnum::Provider; + } +} diff --git a/src/Laravel/Console/Maker/Resources/skeleton/EloquentFilter.php.tpl b/src/Laravel/Console/Maker/Resources/skeleton/EloquentFilter.php.tpl new file mode 100644 index 00000000000..4ca1ac477b8 --- /dev/null +++ b/src/Laravel/Console/Maker/Resources/skeleton/EloquentFilter.php.tpl @@ -0,0 +1,22 @@ + $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + // TODO: make your awesome query using the $builder + // return $builder-> + } +} diff --git a/src/Laravel/Console/Maker/Resources/skeleton/StateProcessor.php.tpl b/src/Laravel/Console/Maker/Resources/skeleton/StateProcessor.php.tpl new file mode 100644 index 00000000000..3dfbe22549c --- /dev/null +++ b/src/Laravel/Console/Maker/Resources/skeleton/StateProcessor.php.tpl @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker\Utils; + +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +final readonly class FilterAppServiceProviderTagger +{ + /** @var string */ + private const APP_SERVICE_PROVIDER_PATH = 'Providers/AppServiceProvider.php'; + + /** @var string */ + private const FILTER_INTERFACE_USE_STATEMENT = 'use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface;'; + + public function __construct(private Filesystem $filesystem) + { + } + + /** + * @throws FileNotFoundException + */ + public function addTagToServiceProvider(string $filterName): void + { + $appServiceProviderPath = app_path(self::APP_SERVICE_PROVIDER_PATH); + if (!$this->filesystem->exists($appServiceProviderPath)) { + throw new \RuntimeException('The AppServiceProvider is missing!'); + } + + $serviceProviderContent = $this->filesystem->get($appServiceProviderPath); + + $this->addUseStatements($serviceProviderContent, $filterName); + $this->addTag($serviceProviderContent, $filterName, $appServiceProviderPath); + } + + private function addUseStatements(string &$content, string $filterName): void + { + $useStatements = [self::FILTER_INTERFACE_USE_STATEMENT, \sprintf('use App\\Filter\\%s;', $filterName)]; + $statementsString = implode("\n", $useStatements)."\n"; + + $content = preg_replace( + '/^(namespace\s[^;]+;\s*)/m', + "$1\n$statementsString", + $content, + 1 + ); + } + + private function addTag(string &$content, string $filterName, string $serviceProviderPath): void + { + $tagStatement = \sprintf("\n\n\t\t\$this->app->tag(%s::class, FilterInterface::class);", $filterName); + + if (!str_contains($content, $tagStatement)) { + $content = preg_replace( + '/(public function register\(\)[^{]*{)(.*?)(\s*}\s*})/s', + "$1$2$tagStatement$3", + $content + ); + + $this->filesystem->put($serviceProviderPath, $content); + } + } +} diff --git a/src/Laravel/Console/Maker/Utils/FilterTemplateGenerator.php b/src/Laravel/Console/Maker/Utils/FilterTemplateGenerator.php new file mode 100644 index 00000000000..be2733eeee3 --- /dev/null +++ b/src/Laravel/Console/Maker/Utils/FilterTemplateGenerator.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker\Utils; + +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +final readonly class FilterTemplateGenerator +{ + public function __construct(private Filesystem $filesystem) + { + } + + public function getFilePath(string $directoryPath, string $filterFileName): string + { + return \sprintf('%s%s.php', $directoryPath, $filterFileName); + } + + /** + * @throws FileNotFoundException + */ + public function generate(string $pathLink, string $filterName): void + { + $namespace = 'App\\Filter'; + $template = $this->filesystem->get( + \sprintf( + '%s/Resources/skeleton/EloquentFilter.php.tpl', + \dirname(__DIR__), + ) + ); + + $content = strtr($template, [ + '{{ namespace }}' => $namespace, + '{{ class_name }}' => $filterName, + ]); + + $this->filesystem->put($pathLink, $content); + } +} diff --git a/src/Laravel/Console/Maker/Utils/StateAppServiceProviderTagger.php b/src/Laravel/Console/Maker/Utils/StateAppServiceProviderTagger.php new file mode 100644 index 00000000000..a683d46bfdc --- /dev/null +++ b/src/Laravel/Console/Maker/Utils/StateAppServiceProviderTagger.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker\Utils; + +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +final readonly class StateAppServiceProviderTagger +{ + /** @var string */ + private const APP_SERVICE_PROVIDER_PATH = 'Providers/AppServiceProvider.php'; + + /** @var string */ + private const ITEM_PROVIDER_USE_STATEMENT = 'use ApiPlatform\State\ProviderInterface;'; + + /** @var string */ + private const ITEM_PROCESSOR_USE_STATEMENT = 'use ApiPlatform\State\ProcessorInterface;'; + + public function __construct(private Filesystem $filesystem) + { + } + + /** + * @throws FileNotFoundException + */ + public function addTagToServiceProvider(string $providerName, StateTypeEnum $stateTypeEnum): void + { + $appServiceProviderPath = app_path(self::APP_SERVICE_PROVIDER_PATH); + if (!$this->filesystem->exists($appServiceProviderPath)) { + throw new \RuntimeException('The AppServiceProvider is missing!'); + } + + $serviceProviderContent = $this->filesystem->get($appServiceProviderPath); + + $this->addUseStatement($serviceProviderContent, $this->getStateTypeStatement($stateTypeEnum)); + $this->addUseStatement($serviceProviderContent, \sprintf('use App\\State\\%s;', $providerName)); + $this->addTag($serviceProviderContent, $providerName, $appServiceProviderPath, $stateTypeEnum); + } + + private function addUseStatement(string &$content, string $useStatement): void + { + if (!str_contains($content, $useStatement)) { + $content = preg_replace( + '/^(namespace\s[^;]+;\s*)(\n)/m', + "$1\n$useStatement$2", + $content, + 1 + ); + } + } + + private function addTag(string &$content, string $stateName, string $serviceProviderPath, StateTypeEnum $stateTypeEnum): void + { + $tagStatement = \sprintf("\n\n\t\t\$this->app->tag(%s::class, %sInterface::class);", $stateName, $stateTypeEnum->name); + + if (!str_contains($content, $tagStatement)) { + $content = preg_replace( + '/(public function register\(\)[^{]*{)(.*?)(\s*}\s*})/s', + "$1$2$tagStatement$3", + $content + ); + + $this->filesystem->put($serviceProviderPath, $content); + } + } + + private function getStateTypeStatement(StateTypeEnum $stateTypeEnum): string + { + return match ($stateTypeEnum) { + StateTypeEnum::Provider => self::ITEM_PROVIDER_USE_STATEMENT, + StateTypeEnum::Processor => self::ITEM_PROCESSOR_USE_STATEMENT, + }; + } +} diff --git a/src/Laravel/Console/Maker/Utils/StateTemplateGenerator.php b/src/Laravel/Console/Maker/Utils/StateTemplateGenerator.php new file mode 100644 index 00000000000..3751793c58c --- /dev/null +++ b/src/Laravel/Console/Maker/Utils/StateTemplateGenerator.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker\Utils; + +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +final readonly class StateTemplateGenerator +{ + public function __construct(private Filesystem $filesystem) + { + } + + public function getFilePath(string $directoryPath, string $stateFileName): string + { + return $directoryPath.$stateFileName.'.php'; + } + + /** + * @throws FileNotFoundException + */ + public function generate(string $pathLink, string $stateClassName, StateTypeEnum $stateTypeEnum): void + { + $namespace = 'App\\State'; + $template = $this->loadTemplate($stateTypeEnum); + + $content = strtr($template, [ + '{{ namespace }}' => $namespace, + '{{ class_name }}' => $stateClassName, + ]); + + $this->filesystem->put($pathLink, $content); + } + + /** + * @throws FileNotFoundException + */ + private function loadTemplate(StateTypeEnum $stateTypeEnum): string + { + $templateFile = match ($stateTypeEnum) { + StateTypeEnum::Provider => 'StateProvider.php.tpl', + StateTypeEnum::Processor => 'StateProcessor.php.tpl', + }; + + $templatePath = \dirname(__DIR__).'/Resources/skeleton/'.$templateFile; + + return $this->filesystem->get($templatePath); + } +} diff --git a/src/Laravel/Console/Maker/Utils/StateTypeEnum.php b/src/Laravel/Console/Maker/Utils/StateTypeEnum.php new file mode 100644 index 00000000000..a3c97de623c --- /dev/null +++ b/src/Laravel/Console/Maker/Utils/StateTypeEnum.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker\Utils; + +enum StateTypeEnum +{ + case Provider; + case Processor; +} diff --git a/src/Laravel/Console/Maker/Utils/SuccessMessageTrait.php b/src/Laravel/Console/Maker/Utils/SuccessMessageTrait.php new file mode 100644 index 00000000000..d06ae7febac --- /dev/null +++ b/src/Laravel/Console/Maker/Utils/SuccessMessageTrait.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Console\Maker\Utils; + +trait SuccessMessageTrait +{ + private function writeSuccessMessage(string $filePath, string $customText): void + { + $this->newLine(); + $this->line(' '); + $this->line(' Success! '); + $this->line(' '); + $this->newLine(); + $this->line('created: '.$filePath.''); + $this->newLine(); + $this->line("Next: Open your new $customText class and start customizing it."); + } +} diff --git a/src/Laravel/Controller/ApiPlatformController.php b/src/Laravel/Controller/ApiPlatformController.php new file mode 100644 index 00000000000..d7c59b6f8f7 --- /dev/null +++ b/src/Laravel/Controller/ApiPlatformController.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Controller; + +use ApiPlatform\HttpCache\PurgerInterface; +use ApiPlatform\Laravel\Eloquent\Listener\PurgeHttpCacheListener; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Http\Request; +use Illuminate\Routing\Controller; +use Illuminate\Support\Facades\Event; +use Symfony\Component\HttpFoundation\Response; + +class ApiPlatformController extends Controller +{ + /** + * @param ProviderInterface $provider + * @param ProcessorInterface|object|null, Response> $processor + */ + public function __construct( + protected OperationMetadataFactoryInterface $operationMetadataFactory, + protected ProviderInterface $provider, + protected ProcessorInterface $processor, + ) { + } + + /** + * Display a listing of the resource. + */ + public function __invoke(Request $request): Response + { + $operation = $request->attributes->get('_api_operation'); + if (!$operation) { + throw new \RuntimeException('Operation not found.'); + } + + if (!$operation instanceof HttpOperation) { + throw new \LogicException('Operation is not an HttpOperation.'); + } + + $uriVariables = $this->getUriVariables($request, $operation); + $request->attributes->set('_api_uri_variables', $uriVariables); + // at some point we could introduce that back + // if ($this->uriVariablesConverter) { + // $context = ['operation' => $operation, 'uri_variables_map' => $uriVariablesMap]; + // $identifiers = $this->uriVariablesConverter->convert($identifiers, $operation->getClass() ?? $resourceClass, $context); + // } + + $context = [ + 'request' => $request, + 'uri_variables' => $uriVariables, + 'resource_class' => $operation->getClass(), + ]; + + if (null === $operation->canValidate()) { + $operation = $operation->withValidate(!$request->isMethodSafe() && !$request->isMethod('DELETE')); + } + + if (null === $operation->canRead()) { + $operation = $operation->withRead($operation->getUriVariables() || $request->isMethodSafe()); + } + + if (null === $operation->canDeserialize()) { + $operation = $operation->withDeserialize(\in_array($operation->getMethod(), ['POST', 'PUT', 'PATCH'], true)); + } + + $body = $this->provider->provide($operation, $uriVariables, $context); + + // The provider can change the Operation, extract it again from the Request attributes + if ($request->attributes->get('_api_operation') !== $operation) { + $operation = $request->attributes->get('_api_operation'); + $uriVariables = $this->getUriVariables($request, $operation); + } + + $context['previous_data'] = $request->attributes->get('previous_data'); + $context['data'] = $request->attributes->get('data'); + + if (null === $operation->canWrite()) { + $operation = $operation->withWrite(!$request->isMethodSafe()); + } + + if (null === $operation->canSerialize()) { + $operation = $operation->withSerialize(true); + } + + if (interface_exists(PurgerInterface::class)) { + Event::listen('eloquent.saved: *', [PurgeHttpCacheListener::class, 'handleModelSaved']); + Event::listen('eloquent.deleted: *', [PurgeHttpCacheListener::class, 'handleModelDeleted']); + } + + return $this->processor->process($body, $operation, $uriVariables, $context); + } + + /** + * @return array + */ + private function getUriVariables(Request $request, HttpOperation $operation): array + { + $uriVariables = []; + foreach ($operation->getUriVariables() ?? [] as $parameterName => $_) { + // TODO: use $link->getParameterName() instead but be sure it is filled correctly in metadata fist + $parameter = $request->route((string) $parameterName); + if (\is_string($parameter) && ($format = $request->attributes->get('_format')) && str_contains($parameter, $format)) { + $parameter = substr($parameter, 0, \strlen($parameter) - (\strlen($format) + 1)); + } + + $uriVariables[(string) $parameterName] = $parameter; + } + + return $uriVariables; + } +} diff --git a/src/Laravel/Controller/DocumentationController.php b/src/Laravel/Controller/DocumentationController.php new file mode 100644 index 00000000000..0b3b1809b74 --- /dev/null +++ b/src/Laravel/Controller/DocumentationController.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Controller; + +use ApiPlatform\Documentation\Documentation; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Util\ContentNegotiationTrait; +use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; +use ApiPlatform\OpenApi\OpenApi; +use ApiPlatform\OpenApi\Serializer\ApiGatewayNormalizer; +use ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer; +use ApiPlatform\OpenApi\Serializer\OpenApiNormalizer; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\ProviderInterface; +use Negotiation\Negotiator; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Generates the API documentation. + * + * @author Amrouche Hamza + */ +final class DocumentationController +{ + use ContentNegotiationTrait; + + /** + * @param array $documentationFormats + * @param ProviderInterface $provider + * @param ProcessorInterface $processor + */ + public function __construct( + private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, + private readonly string $title = '', + private readonly string $description = '', + private readonly string $version = '', + private readonly ?OpenApiFactoryInterface $openApiFactory = null, + private readonly ?ProviderInterface $provider = null, + private readonly ?ProcessorInterface $processor = null, + ?Negotiator $negotiator = null, + private readonly array $documentationFormats = [OpenApiNormalizer::JSON_FORMAT => ['application/vnd.openapi+json'], OpenApiNormalizer::FORMAT => ['application/json']], + private readonly bool $swaggerUiEnabled = true, + ) { + $this->negotiator = $negotiator ?? new Negotiator(); + } + + public function __invoke(Request $request): Response + { + $context = [ + 'api_gateway' => $request->query->getBoolean(ApiGatewayNormalizer::API_GATEWAY), + 'base_url' => $request->getBaseUrl(), + 'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION), + ]; + $request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context); + // We want to find the format early on, this code is also executed later on by the ContentNegotiationProvider. + $this->addRequestFormats($request, $this->documentationFormats); + $format = $this->getRequestFormat($request, $this->documentationFormats); + + if ('html' === $format || OpenApiNormalizer::FORMAT === $format || OpenApiNormalizer::JSON_FORMAT === $format || OpenApiNormalizer::YAML_FORMAT === $format) { + return $this->getOpenApiDocumentation($context, $format, $request); + } + + return $this->getHydraDocumentation($context, $request); + } + + /** + * @param array $context + */ + private function getOpenApiDocumentation(array $context, string $format, Request $request): Response + { + $context['request'] = $request; + $operation = new Get( + class: OpenApi::class, + read: true, + serialize: true, + provider: fn () => $this->openApiFactory->__invoke($context), + normalizationContext: [ + ApiGatewayNormalizer::API_GATEWAY => $context['api_gateway'] ?? null, + LegacyOpenApiNormalizer::SPEC_VERSION => $context['spec_version'] ?? null, + ], + outputFormats: $this->documentationFormats + ); + + if ('html' === $format && $this->swaggerUiEnabled) { + $operation = $operation->withProcessor('api_platform.swagger_ui.processor')->withWrite(true); + } + + return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context); + } + + /** + * TODO: the logic behind the Hydra Documentation is done in a ApiPlatform\Hydra\Serializer\DocumentationNormalizer. + * We should transform this to a provider, it'd improve performances also by a bit. + * + * @param array $context + */ + private function getHydraDocumentation(array $context, Request $request): Response + { + $context['request'] = $request; + $operation = new Get( + class: Documentation::class, + read: true, + serialize: true, + provider: fn () => new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version) + ); + + return $this->processor->process($this->provider->provide($operation, [], $context), $operation, [], $context); + } +} diff --git a/src/Laravel/Controller/EntrypointController.php b/src/Laravel/Controller/EntrypointController.php new file mode 100644 index 00000000000..d013352c0b6 --- /dev/null +++ b/src/Laravel/Controller/EntrypointController.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Controller; + +use ApiPlatform\Documentation\Entrypoint; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceNameCollection; +use ApiPlatform\OpenApi\Serializer\LegacyOpenApiNormalizer; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\ProviderInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Generates the API entrypoint. + * + * @author Kévin Dunglas + */ +final class EntrypointController +{ + private static ResourceNameCollection $resourceNameCollection; + + /** + * @param array $documentationFormats + * @param ProviderInterface $provider + * @param ProcessorInterface $processor + */ + public function __construct( + private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, + private readonly ProviderInterface $provider, + private readonly ProcessorInterface $processor, + private readonly array $documentationFormats = [], + ) { + } + + public function __invoke(Request $request): Response + { + self::$resourceNameCollection = $this->resourceNameCollectionFactory->create(); + $context = [ + 'request' => $request, + 'spec_version' => (string) $request->query->get(LegacyOpenApiNormalizer::SPEC_VERSION), + ]; + $request->attributes->set('_api_platform_disable_listeners', true); + $operation = new Get( + outputFormats: $this->documentationFormats, + read: true, + serialize: true, + class: Entrypoint::class, + provider: [self::class, 'provide'] + ); + $request->attributes->set('_api_operation', $operation); + $body = $this->provider->provide($operation, [], $context); + $operation = $request->attributes->get('_api_operation'); + + return $this->processor->process($body, $operation, [], $context); + } + + public static function provide(): Entrypoint + { + return new Entrypoint(self::$resourceNameCollection); + } +} diff --git a/src/Laravel/Eloquent/ApiPlatformEventProvider.php b/src/Laravel/Eloquent/ApiPlatformEventProvider.php new file mode 100644 index 00000000000..9b9c9ff2474 --- /dev/null +++ b/src/Laravel/Eloquent/ApiPlatformEventProvider.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent; + +use ApiPlatform\HttpCache\PurgerInterface; +use ApiPlatform\HttpCache\SouinPurger; +use ApiPlatform\HttpCache\VarnishPurger; +use ApiPlatform\HttpCache\VarnishXKeyPurger; +use ApiPlatform\Laravel\Eloquent\Listener\PurgeHttpCacheListener; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Foundation\Http\Events\RequestHandled; +use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; +use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Event; +use Symfony\Component\HttpClient\HttpClient; + +class ApiPlatformEventProvider extends ServiceProvider +{ + /** + * @var array> + */ + protected $listen = []; + + public function register(): void + { + if (!interface_exists(PurgerInterface::class)) { + return; + } + + $this->app->singleton('api_platform.http_cache.clients_array', function (Application $app) { + $purgerUrls = Config::get('api-platform.http_cache.invalidation.urls', []); + $requestOptions = Config::get('api-platform.http_cache.invalidation.request_options', []); + + $clients = []; + foreach ($purgerUrls as $url) { + $clients[] = HttpClient::create(array_merge($requestOptions, ['base_uri' => $url])); + } + + return $clients; + }); + + $httpClients = fn (Application $app) => $app->make('api_platform.http_cache.clients_array'); + + $this->app->singleton(VarnishPurger::class, function (Application $app) use ($httpClients) { + return new VarnishPurger($httpClients($app)); + }); + + $this->app->singleton(VarnishXKeyPurger::class, function (Application $app) use ($httpClients) { + return new VarnishXKeyPurger( + $httpClients($app), + Config::get('api-platform.http_cache.invalidation.max_header_length', 7500), + Config::get('api-platform.http_cache.invalidation.xkey.glue', ' ') + ); + }); + + $this->app->singleton(SouinPurger::class, function (Application $app) use ($httpClients) { + return new SouinPurger( + $httpClients($app), + Config::get('api-platform.http_cache.invalidation.max_header_length', 7500) + ); + }); + + $this->app->singleton(PurgerInterface::class, function (Application $app) { + $purgerClass = Config::get( + 'api-platform.http_cache.invalidation.purger', + SouinPurger::class + ); + + if (!class_exists($purgerClass)) { + throw new \InvalidArgumentException("Purger class '{$purgerClass}' configured in api-platform.php was not found."); + } + + return $app->make($purgerClass); + }); + + $this->app->singleton(PurgeHttpCacheListener::class, function (Application $app) { + return new PurgeHttpCacheListener( + $app->make(PurgerInterface::class), + $app->make(IriConverterInterface::class), + $app->make(ResourceClassResolverInterface::class) + ); + }); + } + + public function boot(): void + { + if (!interface_exists(PurgerInterface::class)) { + return; + } + + Event::listen(RequestHandled::class, function (): void { + Event::forget('eloquent.saved: *'); + Event::forget('eloquent.deleted: *'); + $this->app->make(PurgeHttpCacheListener::class)->postFlush(); + }); + } + + public function shouldDiscoverEvents(): bool + { + return false; + } +} diff --git a/src/Laravel/Eloquent/Extension/FilterQueryExtension.php b/src/Laravel/Eloquent/Extension/FilterQueryExtension.php new file mode 100644 index 00000000000..40929ead511 --- /dev/null +++ b/src/Laravel/Eloquent/Extension/FilterQueryExtension.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Extension; + +use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ParameterNotFound; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; +use Psr\Container\ContainerInterface; + +final readonly class FilterQueryExtension implements QueryExtensionInterface +{ + public function __construct( + private ContainerInterface $filterLocator, + ) { + } + + /** + * @param Builder $builder + * @param array $uriVariables + * @param array $context + * + * @return Builder + */ + public function apply(Builder $builder, array $uriVariables, Operation $operation, $context = []): Builder + { + if (!$operation instanceof CollectionOperationInterface) { + return $builder; + } + + $context['uri_variables'] = $uriVariables; + $context['operation'] = $operation; + + foreach ($operation->getParameters() ?? [] as $parameter) { + if (null === ($values = $parameter->getValue()) || $values instanceof ParameterNotFound) { + continue; + } + + if (null === ($filterId = $parameter->getFilter())) { + continue; + } + + // most eloquent filters work with only a single value + if (\is_array($values) && array_is_list($values) && 1 === \count($values)) { + $values = current($values); + } + + $filter = $filterId instanceof FilterInterface ? $filterId : ($this->filterLocator->has($filterId) ? $this->filterLocator->get($filterId) : null); + if ($filter instanceof FilterInterface) { + $builder = $filter->apply($builder, $values, $parameter->withKey($parameter->getExtraProperties()['_query_property'] ?? $parameter->getKey()), $context + ($parameter->getFilterContext() ?? [])); + } + } + + return $builder; + } +} diff --git a/src/Laravel/Eloquent/Extension/QueryExtensionInterface.php b/src/Laravel/Eloquent/Extension/QueryExtensionInterface.php new file mode 100644 index 00000000000..1b36f27dbf5 --- /dev/null +++ b/src/Laravel/Eloquent/Extension/QueryExtensionInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Extension; + +use ApiPlatform\Metadata\Operation; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +interface QueryExtensionInterface +{ + /** + * @param Builder $builder + * @param array $uriVariables + * @param array $context + * + * @return Builder + */ + public function apply(Builder $builder, array $uriVariables, Operation $operation, $context = []): Builder; +} diff --git a/src/Laravel/Eloquent/Filter/BooleanFilter.php b/src/Laravel/Eloquent/Filter/BooleanFilter.php new file mode 100644 index 00000000000..9879c2212f6 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/BooleanFilter.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class BooleanFilter implements FilterInterface, JsonSchemaFilterInterface +{ + use QueryPropertyTrait; + + private const BOOLEAN_VALUES = [ + 'true' => true, + 'false' => false, + '1' => true, + '0' => false, + ]; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + if (!\is_string($values) || !\array_key_exists($values, self::BOOLEAN_VALUES)) { + return $builder; + } + + return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), $values); + } + + public function getSchema(Parameter $parameter): array + { + return ['type' => 'boolean']; + } +} diff --git a/src/Laravel/Eloquent/Filter/DateFilter.php b/src/Laravel/Eloquent/Filter/DateFilter.php new file mode 100644 index 00000000000..29903f50d44 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/DateFilter.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class DateFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface +{ + use QueryPropertyTrait; + + private const OPERATOR_VALUE = [ + 'eq' => '=', + 'gt' => '>', + 'lt' => '<', + 'gte' => '>=', + 'lte' => '<=', + ]; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + if (!\is_array($values)) { + return $builder; + } + + $values = array_intersect_key($values, self::OPERATOR_VALUE); + + if (!$values) { + return $builder; + } + + if (true === ($parameter->getFilterContext()['include_nulls'] ?? false)) { + foreach ($values as $key => $value) { + $datetime = $this->getDateTime($value); + if (null === $datetime) { + continue; + } + $builder->{$context['whereClause'] ?? 'where'}(function (Builder $query) use ($parameter, $datetime, $key): void { + $queryProperty = $this->getQueryProperty($parameter); + $query->whereDate($queryProperty, self::OPERATOR_VALUE[$key], $datetime) + ->orWhereNull($queryProperty); + }); + } + + return $builder; + } + + foreach ($values as $key => $value) { + $datetime = $this->getDateTime($value); + if (null === $datetime) { + continue; + } + $builder = $builder->{($context['whereClause'] ?? 'where').'Date'}($this->getQueryProperty($parameter), self::OPERATOR_VALUE[$key], $datetime); + } + + return $builder; + } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'date']; + } + + /** + * @return OpenApiParameter[] + */ + public function getOpenApiParameters(Parameter $parameter): array + { + $in = $parameter instanceof QueryParameter ? 'query' : 'header'; + $key = $parameter->getKey(); + + return [ + new OpenApiParameter(name: $key.'[eq]', in: $in), + new OpenApiParameter(name: $key.'[gt]', in: $in), + new OpenApiParameter(name: $key.'[lt]', in: $in), + new OpenApiParameter(name: $key.'[gte]', in: $in), + new OpenApiParameter(name: $key.'[lte]', in: $in), + ]; + } + + private function getDateTime(string $value): ?\DateTimeImmutable + { + try { + return new \DateTimeImmutable($value); + } catch (\DateMalformedStringException|\Exception) { + return null; + } + } +} diff --git a/src/Laravel/Eloquent/Filter/EndSearchFilter.php b/src/Laravel/Eloquent/Filter/EndSearchFilter.php new file mode 100644 index 00000000000..7b70210d1ee --- /dev/null +++ b/src/Laravel/Eloquent/Filter/EndSearchFilter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class EndSearchFilter implements FilterInterface +{ + use QueryPropertyTrait; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), 'like', '%'.$values); + } +} diff --git a/src/Laravel/Eloquent/Filter/EqualsFilter.php b/src/Laravel/Eloquent/Filter/EqualsFilter.php new file mode 100644 index 00000000000..d95d13d6751 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/EqualsFilter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class EqualsFilter implements FilterInterface +{ + use QueryPropertyTrait; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), $values); + } +} diff --git a/src/Laravel/Eloquent/Filter/FilterInterface.php b/src/Laravel/Eloquent/Filter/FilterInterface.php new file mode 100644 index 00000000000..b3233b99830 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/FilterInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +interface FilterInterface +{ + /** + * @param Builder $builder + * @param array $context + * + * @return Builder + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder; +} diff --git a/src/Laravel/Eloquent/Filter/JsonApi/SortFilter.php b/src/Laravel/Eloquent/Filter/JsonApi/SortFilter.php new file mode 100644 index 00000000000..46292a28c8a --- /dev/null +++ b/src/Laravel/Eloquent/Filter/JsonApi/SortFilter.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter\JsonApi; + +use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface; +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\ParameterProviderFilterInterface; +use ApiPlatform\Metadata\PropertiesAwareInterface; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class SortFilter implements FilterInterface, JsonSchemaFilterInterface, ParameterProviderFilterInterface, PropertiesAwareInterface +{ + public const ASC = 'asc'; + public const DESC = 'desc'; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + if (!\is_array($values)) { + return $builder; + } + + foreach ($values as $order => $dir) { + if (self::ASC !== $dir && self::DESC !== $dir) { + continue; + } + + $builder->orderBy($order, $dir); + } + + return $builder; + } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'string']; + } + + public static function getParameterProvider(): string + { + return SortFilterParameterProvider::class; + } +} diff --git a/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php b/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php new file mode 100644 index 00000000000..afa2e10674f --- /dev/null +++ b/src/Laravel/Eloquent/Filter/JsonApi/SortFilterParameterProvider.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter\JsonApi; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\State\ParameterProviderInterface; + +final readonly class SortFilterParameterProvider implements ParameterProviderInterface +{ + public function provide(Parameter $parameter, array $parameters = [], array $context = []): ?Operation + { + if (!($operation = $context['operation'] ?? null)) { + return null; + } + + $parameters = $operation->getParameters(); + $properties = $parameter->getExtraProperties()['_properties'] ?? []; + $value = $parameter->getValue(); + + // most eloquent filters work with only a single value + if (\is_array($value) && array_is_list($value) && 1 === \count($value)) { + $value = current($value); + } + + if (!\is_string($value)) { + return $operation; + } + + $values = explode(',', $value); + $orderBy = []; + foreach ($values as $v) { + $dir = SortFilter::ASC; + if (str_starts_with($v, '-')) { + $dir = SortFilter::DESC; + $v = substr($v, 1); + } + + if (\array_key_exists($v, $properties)) { + $orderBy[$properties[$v]] = $dir; + } + } + + $parameters->add($parameter->getKey(), $parameter->withExtraProperties( + ['_api_values' => $orderBy] + $parameter->getExtraProperties() + )); + + return $operation->withParameters($parameters); + } +} diff --git a/src/Laravel/Eloquent/Filter/OrFilter.php b/src/Laravel/Eloquent/Filter/OrFilter.php new file mode 100644 index 00000000000..609608cb929 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/OrFilter.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final readonly class OrFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface +{ + public function __construct(private FilterInterface $filter) + { + } + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + return $builder->where(function ($builder) use ($values, $parameter, $context): void { + foreach ($values as $value) { + $this->filter->apply($builder, $value, $parameter, ['whereClause' => 'orWhere'] + $context); + } + }); + } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + $schema = $this->filter instanceof JsonSchemaFilterInterface ? $this->filter->getSchema($parameter) : ['type' => 'string']; + + return ['type' => 'array', 'items' => $schema]; + } + + public function getOpenApiParameters(Parameter $parameter): OpenApiParameter + { + return new OpenApiParameter(name: $parameter->getKey().'[]', in: 'query', style: 'deepObject', explode: true); + } +} diff --git a/src/Laravel/Eloquent/Filter/OrderFilter.php b/src/Laravel/Eloquent/Filter/OrderFilter.php new file mode 100644 index 00000000000..233356d48bb --- /dev/null +++ b/src/Laravel/Eloquent/Filter/OrderFilter.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class OrderFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface +{ + use QueryPropertyTrait; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + if (!\is_string($values)) { + $properties = $parameter->getExtraProperties()['_properties'] ?? []; + + foreach ($values as $key => $value) { + if (!isset($properties[$key])) { + continue; + } + $builder = $builder->orderBy($properties[$key], $value); + } + + return $builder; + } + + return $builder->orderBy($this->getQueryProperty($parameter), $values); + } + + /** + * @return array + */ + public function getSchema(Parameter $parameter): array + { + return ['type' => 'string', 'enum' => ['asc', 'desc']]; + } + + /** + * @return OpenApiParameter[]|null + */ + public function getOpenApiParameters(Parameter $parameter): ?array + { + if (str_contains($parameter->getKey(), ':property')) { + $parameters = []; + $key = str_replace('[:property]', '', $parameter->getKey()); + foreach (array_keys($parameter->getExtraProperties()['_properties'] ?? []) as $property) { + $parameters[] = new OpenApiParameter(name: \sprintf('%s[%s]', $key, $property), in: 'query'); + } + + return $parameters; + } + + return null; + } +} diff --git a/src/Laravel/Eloquent/Filter/PartialSearchFilter.php b/src/Laravel/Eloquent/Filter/PartialSearchFilter.php new file mode 100644 index 00000000000..70c03abc62d --- /dev/null +++ b/src/Laravel/Eloquent/Filter/PartialSearchFilter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class PartialSearchFilter implements FilterInterface +{ + use QueryPropertyTrait; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), 'like', '%'.$values.'%'); + } +} diff --git a/src/Laravel/Eloquent/Filter/QueryPropertyTrait.php b/src/Laravel/Eloquent/Filter/QueryPropertyTrait.php new file mode 100644 index 00000000000..3cee8cdc372 --- /dev/null +++ b/src/Laravel/Eloquent/Filter/QueryPropertyTrait.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; + +/** + * @internal + */ +trait QueryPropertyTrait +{ + private function getQueryProperty(Parameter $parameter): ?string + { + return $parameter->getExtraProperties()['_query_property'] ?? $parameter->getProperty() ?? null; + } +} diff --git a/src/Laravel/Eloquent/Filter/RangeFilter.php b/src/Laravel/Eloquent/Filter/RangeFilter.php new file mode 100644 index 00000000000..bb86f7dc78f --- /dev/null +++ b/src/Laravel/Eloquent/Filter/RangeFilter.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\JsonSchemaFilterInterface; +use ApiPlatform\Metadata\OpenApiParameterFilterInterface; +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\QueryParameter; +use ApiPlatform\OpenApi\Model\Parameter as OpenApiParameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class RangeFilter implements FilterInterface, JsonSchemaFilterInterface, OpenApiParameterFilterInterface +{ + use QueryPropertyTrait; + + private const OPERATOR_VALUE = [ + 'lt' => '<', + 'gt' => '>', + 'lte' => '<=', + 'gte' => '>=', + ]; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + $queryProperty = $this->getQueryProperty($parameter); + + foreach ($values as $key => $value) { + $builder = $builder->{$context['whereClause'] ?? 'where'}($queryProperty, self::OPERATOR_VALUE[$key], $value); + } + + return $builder; + } + + public function getSchema(Parameter $parameter): array + { + return ['type' => 'number']; + } + + /** + * @return OpenApiParameter[] + */ + public function getOpenApiParameters(Parameter $parameter): array + { + $in = $parameter instanceof QueryParameter ? 'query' : 'header'; + $key = $parameter->getKey(); + + return [ + new OpenApiParameter(name: $key.'[gt]', in: $in), + new OpenApiParameter(name: $key.'[lt]', in: $in), + new OpenApiParameter(name: $key.'[gte]', in: $in), + new OpenApiParameter(name: $key.'[lte]', in: $in), + ]; + } +} diff --git a/src/Laravel/Eloquent/Filter/StartSearchFilter.php b/src/Laravel/Eloquent/Filter/StartSearchFilter.php new file mode 100644 index 00000000000..b0b20a56b0c --- /dev/null +++ b/src/Laravel/Eloquent/Filter/StartSearchFilter.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Filter; + +use ApiPlatform\Metadata\Parameter; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +final class StartSearchFilter implements FilterInterface +{ + use QueryPropertyTrait; + + /** + * @param Builder $builder + * @param array $context + */ + public function apply(Builder $builder, mixed $values, Parameter $parameter, array $context = []): Builder + { + return $builder->{$context['whereClause'] ?? 'where'}($this->getQueryProperty($parameter), 'like', $values.'%'); + } +} diff --git a/src/Laravel/Eloquent/Listener/PurgeHttpCacheListener.php b/src/Laravel/Eloquent/Listener/PurgeHttpCacheListener.php new file mode 100644 index 00000000000..2be9aae6a6a --- /dev/null +++ b/src/Laravel/Eloquent/Listener/PurgeHttpCacheListener.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Listener; + +use ApiPlatform\HttpCache\PurgerInterface; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\ItemNotFoundException; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Illuminate\Database\Eloquent\Model; + +final class PurgeHttpCacheListener +{ + /** + * @var string[] + */ + private array $tags = []; + + public function __construct( + private readonly PurgerInterface $purger, + private readonly IriConverterInterface $iriConverter, + private readonly ResourceClassResolverInterface $resourceClassResolver, + ) { + } + + /** + * @param Model[] $data + */ + public function handleModelSaved(string $eventName, array $data): void + { + foreach ($data as $model) { + if (!$this->resourceClassResolver->isResourceClass($model::class)) { + return; + } + + try { + $this->tags[] = $this->iriConverter->getIriFromResource($model); + $this->tags[] = $this->iriConverter->getIriFromResource($model::class, operation: new GetCollection(class: $model::class)); + } catch (InvalidArgumentException|ItemNotFoundException $e) { + // do nothing + } + } + } + + /** + * @param Model[] $data + */ + public function handleModelDeleted(string $eventName, array $data): void + { + foreach ($data as $model) { + if (!$this->resourceClassResolver->isResourceClass($model::class)) { + return; + } + + try { + $this->tags[] = $this->iriConverter->getIriFromResource($model); + $this->tags[] = $this->iriConverter->getIriFromResource($model::class, operation: new GetCollection(class: $model::class)); + } catch (InvalidArgumentException|ItemNotFoundException $e) { + // do nothing + } + } + } + + /** + * Purges all collected tags at the end of the request. + */ + public function postFlush(): void + { + if (empty($this->tags)) { + return; + } + + $this->purger->purge(array_values(array_unique($this->tags))); + $this->tags = []; + } +} diff --git a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php new file mode 100644 index 00000000000..51ddb890dd9 --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentAttributePropertyMetadataFactory.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property; + +use ApiPlatform\JsonSchema\Metadata\Property\Factory\SchemaPropertyMetadataFactory; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Exception\PropertyNotFoundException; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use Illuminate\Database\Eloquent\Model; + +/** + * Handles Eloquent methods for relations. + */ +final class EloquentAttributePropertyMetadataFactory implements PropertyMetadataFactoryInterface +{ + public function __construct( + private readonly ?PropertyMetadataFactoryInterface $decorated = null, + ) { + } + + public function create(string $resourceClass, string $property, array $options = []): ApiProperty + { + if (!class_exists($resourceClass)) { + return $this->decorated?->create($resourceClass, $property, $options) ?? + $this->throwNotFound($resourceClass, $property); + } + + $refl = new \ReflectionClass($resourceClass); + $model = $refl->newInstanceWithoutConstructor(); + + $propertyMetadata = $this->decorated?->create($resourceClass, $property, $options); + if (!$model instanceof Model) { + return $propertyMetadata ?? $this->throwNotFound($resourceClass, $property); + } + + if ($refl->hasMethod($property) && $attributes = $refl->getMethod($property)->getAttributes(ApiProperty::class)) { + return $this->createMetadata($attributes[0]->newInstance(), $propertyMetadata); + } + + return $propertyMetadata; + } + + /** + * @throws PropertyNotFoundException + */ + private function throwNotFound(string $resourceClass, string $property): never + { + throw new PropertyNotFoundException(\sprintf('Property "%s" of class "%s" not found.', $property, $resourceClass)); + } + + private function createMetadata(ApiProperty $attribute, ?ApiProperty $propertyMetadata = null): ApiProperty + { + if (null === $propertyMetadata) { + return $this->handleUserDefinedSchema($attribute); + } + + foreach (get_class_methods(ApiProperty::class) as $method) { + if (preg_match('/^(?:get|is)(.*)/', $method, $matches) && null !== $val = $attribute->{$method}()) { + $propertyMetadata = $propertyMetadata->{"with{$matches[1]}"}($val); + } + } + + return $this->handleUserDefinedSchema($propertyMetadata); + } + + private function handleUserDefinedSchema(ApiProperty $propertyMetadata): ApiProperty + { + // can't know later if the schema has been defined by the user or by API Platform + // store extra key to make this difference + if (null !== $propertyMetadata->getSchema()) { + $extraProperties = $propertyMetadata->getExtraProperties(); + $propertyMetadata = $propertyMetadata->withExtraProperties([SchemaPropertyMetadataFactory::JSON_SCHEMA_USER_DEFINED => true] + $extraProperties); + } + + return $propertyMetadata; + } +} diff --git a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php new file mode 100644 index 00000000000..5c0253263b8 --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyMetadataFactory.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property; + +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Exception\PropertyNotFoundException; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; +use Illuminate\Database\Eloquent\Relations\MorphMany; +use Illuminate\Database\Eloquent\Relations\MorphToMany; +use Illuminate\Support\Collection; +use Symfony\Component\TypeInfo\Type; +use Symfony\Component\TypeInfo\TypeIdentifier; + +/** + * Uses Eloquent metadata to populate the identifier property. + * + * @author Kévin Dunglas + */ +final class EloquentPropertyMetadataFactory implements PropertyMetadataFactoryInterface +{ + public function __construct( + private readonly ModelMetadata $modelMetadata, + private readonly ?PropertyMetadataFactoryInterface $decorated = null, + ) { + } + + /** + * {@inheritdoc} + * + * @param class-string $resourceClass + */ + public function create(string $resourceClass, string $property, array $options = []): ApiProperty + { + if (!is_a($resourceClass, Model::class, true)) { + return $this->decorated?->create($resourceClass, $property, $options) ?? new ApiProperty(); + } + + try { + $refl = new \ReflectionClass($resourceClass); + $model = $refl->newInstanceWithoutConstructor(); + } catch (\ReflectionException) { + return $this->decorated?->create($resourceClass, $property, $options) ?? new ApiProperty(); + } + + try { + $propertyMetadata = $this->decorated?->create($resourceClass, $property, $options) ?? new ApiProperty(); + } catch (PropertyNotFoundException) { + $propertyMetadata = new ApiProperty(); + } + + if ($model->getKeyName() === $property) { + $propertyMetadata = $propertyMetadata->withIdentifier(true); + } + + foreach ($this->modelMetadata->getAttributes($model) as $p) { + if ($p['name'] !== $property) { + continue; + } + + // see https://laravel.com/docs/11.x/eloquent-mutators#attribute-casting + $builtinType = $p['cast'] ?? $p['type']; + $type = match ($builtinType) { + 'integer' => Type::int(), + 'double', 'real' => Type::float(), + 'boolean', 'bool' => Type::bool(), + 'datetime', 'date', 'timestamp' => Type::object(\DateTime::class), + 'immutable_datetime', 'immutable_date' => Type::object(\DateTimeImmutable::class), + 'collection', 'encrypted:collection' => Type::collection(Type::object(Collection::class)), + 'encrypted:array' => Type::builtin(TypeIdentifier::ARRAY), + 'encrypted:object' => Type::object(), + default => \in_array($builtinType, TypeIdentifier::values(), true) ? Type::builtin($builtinType) : Type::string(), + }; + + if ($p['nullable']) { + $type = Type::nullable($type); + } + + $propertyMetadata = $propertyMetadata + ->withNativeType($type); + + return $propertyMetadata; + } + + foreach ($this->modelMetadata->getRelations($model) as $relation) { + if ($relation['name'] !== $property) { + continue; + } + + $collection = match ($relation['type']) { + HasMany::class, + HasManyThrough::class, + BelongsToMany::class, + MorphMany::class, + MorphToMany::class => true, + default => false, + }; + + $type = Type::object($relation['related']); + if ($collection) { + $type = Type::iterable($type); + } + + return $propertyMetadata + ->withNativeType($type) + ->withExtraProperties(['eloquent_relation' => $relation] + $propertyMetadata->getExtraProperties()); + } + + return $propertyMetadata; + } +} diff --git a/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php new file mode 100644 index 00000000000..fbff507e905 --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/Factory/Property/EloquentPropertyNameCollectionMetadataFactory.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Property; + +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Illuminate\Database\Eloquent\Model; + +final class EloquentPropertyNameCollectionMetadataFactory implements PropertyNameCollectionFactoryInterface +{ + public function __construct( + private readonly ModelMetadata $modelMetadata, + private readonly ?PropertyNameCollectionFactoryInterface $decorated, + private readonly ResourceClassResolverInterface $resourceClassResolver, + ) { + } + + /** + * {@inheritdoc} + * + * @param class-string $resourceClass + */ + public function create(string $resourceClass, array $options = []): PropertyNameCollection + { + if (!class_exists($resourceClass) || !is_a($resourceClass, Model::class, true)) { + return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection(); + } + + try { + $refl = new \ReflectionClass($resourceClass); + if ($refl->isAbstract()) { + return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection(); + } + + $model = $refl->newInstanceWithoutConstructor(); + } catch (\ReflectionException) { + return $this->decorated?->create($resourceClass, $options) ?? new PropertyNameCollection(); + } + + /** + * @var array $properties + */ + $properties = []; + + // When it's an Eloquent model we read attributes from database (@see ShowModelCommand) + foreach ($this->modelMetadata->getAttributes($model) as $property) { + if (!($property['primary'] ?? null) && $property['hidden']) { + continue; + } + + $properties[$property['name']] = true; + } + + foreach ($this->modelMetadata->getRelations($model) as $relation) { + if (!$this->resourceClassResolver->isResourceClass($relation['related'])) { + continue; + } + + $properties[$relation['name']] = true; + } + + return new PropertyNameCollection( + array_keys($properties) + ); + } +} diff --git a/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php b/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php new file mode 100644 index 00000000000..551a08576aa --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/Factory/Resource/EloquentResourceCollectionMetadataFactory.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata\Factory\Resource; + +use ApiPlatform\Laravel\Eloquent\State\CollectionProvider; +use ApiPlatform\Laravel\Eloquent\State\ItemProvider; +use ApiPlatform\Laravel\Eloquent\State\PersistProcessor; +use ApiPlatform\Laravel\Eloquent\State\RemoveProcessor; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\DeleteOperationInterface; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\DeleteMutation; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Metadata\GraphQl\Subscription; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Gate; + +final class EloquentResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface +{ + private const POLICY_METHODS = [ + Put::class => 'update', + Post::class => 'create', + Get::class => 'view', + GetCollection::class => 'viewAny', + Delete::class => 'delete', + Patch::class => 'update', + + Query::class => 'view', + QueryCollection::class => 'viewAny', + Mutation::class => 'update', + DeleteMutation::class => 'delete', + Subscription::class => 'viewAny', + ]; + + public function __construct( + private readonly ResourceMetadataCollectionFactoryInterface $decorated, + ) { + } + + /** + * @param class-string $resourceClass + */ + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = $this->decorated->create($resourceClass); + + try { + $refl = new \ReflectionClass($resourceClass); + $model = $refl->newInstanceWithoutConstructor(); + } catch (\ReflectionException) { + return $this->decorated->create($resourceClass); + } + + if (!$model instanceof Model) { + return $resourceMetadataCollection; + } + + foreach ($resourceMetadataCollection as $i => $resourceMetadata) { + $operations = $resourceMetadata->getOperations(); + foreach ($operations ?? [] as $operationName => $operation) { + if (!$operation->getProvider()) { + $operation = $operation->withProvider($operation instanceof CollectionOperationInterface ? CollectionProvider::class : ItemProvider::class); + } + + if (!$operation->getPolicy() && ($policy = Gate::getPolicyFor($model))) { + $policyMethod = self::POLICY_METHODS[$operation::class] ?? null; + if ($operation instanceof Put && $operation->getAllowCreate()) { + $policyMethod = self::POLICY_METHODS[Post::class]; + } + + if ($policyMethod && method_exists($policy, $policyMethod)) { + $operation = $operation->withPolicy($policyMethod); + } + } + + if (!$operation->getProcessor()) { + $operation = $operation->withProcessor($operation instanceof DeleteOperationInterface ? RemoveProcessor::class : PersistProcessor::class); + } + + $operations->add($operationName, $operation); + } + + $resourceMetadataCollection[$i] = $resourceMetadata->withOperations($operations); + + $graphQlOperations = $resourceMetadata->getGraphQlOperations(); + foreach ($graphQlOperations ?? [] as $operationName => $graphQlOperation) { + if (!$graphQlOperation->getPolicy() && ($policy = Gate::getPolicyFor($model))) { + if (($policyMethod = self::POLICY_METHODS[$graphQlOperation::class] ?? null) && method_exists($policy, $policyMethod)) { + $graphQlOperation = $graphQlOperation->withPolicy($policyMethod); + } + } + + if (!$graphQlOperation->getProvider()) { + $graphQlOperation = $graphQlOperation->withProvider($graphQlOperation instanceof CollectionOperationInterface ? CollectionProvider::class : ItemProvider::class); + } + + if (!$graphQlOperation->getProcessor()) { + $graphQlOperation = $graphQlOperation->withProcessor($graphQlOperation instanceof DeleteOperationInterface ? RemoveProcessor::class : PersistProcessor::class); + } + + $graphQlOperations[$operationName] = $graphQlOperation; + } + + if ($graphQlOperations) { + $resourceMetadata = $resourceMetadata->withGraphQlOperations($graphQlOperations); + } + + $resourceMetadataCollection[$i] = $resourceMetadata; + } + + return $resourceMetadataCollection; + } +} diff --git a/src/Laravel/Eloquent/Metadata/IdentifiersExtractor.php b/src/Laravel/Eloquent/Metadata/IdentifiersExtractor.php new file mode 100644 index 00000000000..e9f1fbbcc60 --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/IdentifiersExtractor.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Operation; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +final class IdentifiersExtractor implements IdentifiersExtractorInterface +{ + public function __construct( + private readonly IdentifiersExtractorInterface $inner, + ) { + } + + public function getIdentifiersFromItem(object $item, ?Operation $operation = null, array $context = []): array + { + if (!($item instanceof BelongsTo || $item instanceof Model) || !$operation instanceof HttpOperation) { + return $this->inner->getIdentifiersFromItem($item, $operation, $context); + } + + $identifiers = []; + foreach ($operation->getUriVariables() ?? [] as $link) { + $parameterName = $link->getParameterName(); + $identifiers[$parameterName] = $this->getIdentifierValue($item, $link); + } + + return $identifiers; + } + + private function getIdentifierValue(object $item, Link $link): mixed + { + if (is_a($item, $link->getFromClass(), true)) { + return $this->getEloquentProperty($item, $link->getIdentifiers()[0]); + } + + if ($item instanceof BelongsTo) { + return $this->getEloquentProperty($item->getParent(), $item->getForeignKeyName()); + } + + if ($toProperty = $link->getToProperty()) { + $relation = $this->getEloquentProperty($item, $toProperty); + + if ($relation instanceof BelongsTo) { + return $this->getEloquentProperty($item, $relation->getForeignKeyName()); + } + } + + return $this->getEloquentProperty($item, $link->getIdentifiers()[0]); + } + + private function getEloquentProperty(object $item, string $property): mixed + { + if (method_exists($item, $property)) { + return $item->{$property}(); + } + + $getter = 'get'.ucfirst($property); + if (method_exists($item, $getter)) { + return $item->{$getter}(); + } + + return $item->{$property}; + } +} diff --git a/src/Laravel/Eloquent/Metadata/ModelMetadata.php b/src/Laravel/Eloquent/Metadata/ModelMetadata.php new file mode 100644 index 00000000000..3fc977f1241 --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/ModelMetadata.php @@ -0,0 +1,303 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Support\Collection; +use Illuminate\Support\Str; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +/** + * Inspired from Illuminate\Database\Console\ShowModelCommand. + * + * @internal + */ +final class ModelMetadata +{ + /** + * @var array> + */ + private $attributesLocalCache = []; + + /** + * @var array> + */ + private $relationsLocalCache = []; + + /** + * The methods that can be called in a model to indicate a relation. + * + * @var string[] + */ + public const RELATION_METHODS = [ + 'hasMany', + 'hasManyThrough', + 'hasOneThrough', + 'belongsToMany', + 'hasOne', + 'belongsTo', + 'morphOne', + 'morphTo', + 'morphMany', + 'morphToMany', + 'morphedByMany', + ]; + + public function __construct(private NameConverterInterface $relationNameConverter = new CamelCaseToSnakeCaseNameConverter()) + { + } + + /** + * Gets the column attributes for the given model. + * + * @return Collection + */ + public function getAttributes(Model $model): Collection + { + if (isset($this->attributesLocalCache[$model::class])) { + return $this->attributesLocalCache[$model::class]; + } + + $connection = $model->getConnection(); + $schema = $connection->getSchemaBuilder(); + $table = $model->getTable(); + $columns = $schema->getColumns($table); + $indexes = $schema->getIndexes($table); + $relations = $this->getRelations($model); + + return $this->attributesLocalCache[$model::class] = collect($columns) + ->reject( + fn ($column) => $relations->contains( + fn ($relation) => $relation['foreign_key'] === $column['name'] + ) + ) + ->map(fn ($column) => [ + 'name' => $column['name'], + 'type' => $column['type'], + 'increments' => $column['auto_increment'], + 'nullable' => $column['nullable'], + 'default' => $this->getColumnDefault($column, $model), + 'unique' => $this->columnIsUnique($column['name'], $indexes), + 'fillable' => $model->isFillable($column['name']), + 'hidden' => $this->attributeIsHidden($column['name'], $model), + 'appended' => null, + 'cast' => $this->getCastType($column['name'], $model), + 'primary' => $this->isColumnPrimaryKey($indexes, $column['name']), + ]) + ->merge($this->getVirtualAttributes($model, $columns)); + } + + /** + * @param array $indexes + */ + private function isColumnPrimaryKey(array $indexes, string $column): bool + { + foreach ($indexes as $index) { + if (\in_array($column, $index['columns'], true) && (true === ($index['primary'] ?? false))) { + return true; + } + } + + return false; + } + + /** + * Get the virtual (non-column) attributes for the given model. + * + * @param array $columns + * + * @return Collection + */ + private function getVirtualAttributes(Model $model, array $columns): Collection + { + $class = new \ReflectionClass($model); + + return collect($class->getMethods()) + ->reject( + fn (\ReflectionMethod $method) => $method->isStatic() + || $method->isAbstract() + || Model::class === $method->getDeclaringClass()->getName() + ) + ->mapWithKeys(function (\ReflectionMethod $method) use ($model) { + if (1 === preg_match('/^get(.+)Attribute$/', $method->getName(), $matches)) { + return [Str::snake($matches[1]) => 'accessor']; + } + if ($model->hasAttributeMutator($method->getName())) { + return [Str::snake($method->getName()) => 'attribute']; + } + + return []; + }) + ->reject(fn ($cast, $name) => collect($columns)->contains('name', $name)) + ->map(fn ($cast, $name) => [ + 'name' => $name, + 'type' => null, + 'increments' => false, + 'nullable' => null, + 'default' => null, + 'unique' => null, + 'fillable' => $model->isFillable($name), + 'hidden' => $this->attributeIsHidden($name, $model), + 'appended' => $model->hasAppended($name), + 'cast' => $cast, + ]) + ->values(); + } + + /** + * Gets the relations from the given model. + * + * @return Collection + */ + public function getRelations(Model $model): Collection + { + if (isset($this->relationsLocalCache[$model::class])) { + return $this->relationsLocalCache[$model::class]; + } + + return $this->relationsLocalCache[$model::class] = collect(get_class_methods($model)) + ->map(fn ($method) => new \ReflectionMethod($model, $method)) + ->reject( + fn (\ReflectionMethod $method) => $method->isStatic() + || $method->isAbstract() + || Model::class === $method->getDeclaringClass()->getName() + || $method->getNumberOfParameters() > 0 + || $this->attributeIsHidden($method->getName(), $model) + ) + ->filter(function (\ReflectionMethod $method) { + if ( + $method->getReturnType() instanceof \ReflectionNamedType + && is_subclass_of($method->getReturnType()->getName(), Relation::class) + ) { + return true; + } + + if (false === $method->getFileName()) { + return false; + } + + $file = new \SplFileObject($method->getFileName()); + $file->seek($method->getStartLine() - 1); + $code = ''; + while ($file->key() < $method->getEndLine()) { + $current = $file->current(); + if (\is_string($current)) { + $code .= trim($current); + } + + $file->next(); + } + + return collect(self::RELATION_METHODS) + ->contains(fn ($relationMethod) => str_contains($code, '$this->'.$relationMethod.'(')); + }) + ->map(function (\ReflectionMethod $method) use ($model) { + $relation = $method->invoke($model); + + if (!$relation instanceof Relation) { + return null; + } + + return [ + 'name' => $this->relationNameConverter->normalize($method->getName()), + 'method_name' => $method->getName(), + 'type' => $relation::class, + 'related' => \get_class($relation->getRelated()), + 'foreign_key' => method_exists($relation, 'getForeignKeyName') ? $relation->getForeignKeyName() : null, + ]; + }) + ->filter() + ->values(); + } + + /** + * Gets the cast type for the given column. + */ + private function getCastType(string $column, Model $model): ?string + { + if ($model->hasGetMutator($column) || $model->hasSetMutator($column)) { + return 'accessor'; + } + + if ($model->hasAttributeMutator($column)) { + return 'attribute'; + } + + return $this->getCastsWithDates($model)->get($column) ?? null; + } + + /** + * Gets the model casts, including any date casts. + * + * @return Collection + */ + private function getCastsWithDates(Model $model): Collection + { + return collect($model->getDates()) + ->filter() + ->flip() + ->map(fn () => 'datetime') + ->merge($model->getCasts()); + } + + /** + * Gets the default value for the given column. + * + * @param array $column + * + * @phpstan-param array $column + * + * @psalm-param array{name: string, default: string, ...} $column + */ + private function getColumnDefault(array $column, Model $model): mixed + { + $attributeDefault = $model->getAttributes()[$column['name']] ?? null; + + return match (true) { + $attributeDefault instanceof \BackedEnum => $attributeDefault->value, + $attributeDefault instanceof \UnitEnum => $attributeDefault->name, + default => $attributeDefault ?? $column['default'], + }; + } + + /** + * Determines if the given attribute is hidden. + */ + private function attributeIsHidden(string $attribute, Model $model): bool + { + if ($visible = $model->getVisible()) { + return !\in_array($attribute, $visible, true); + } + + if ($hidden = $model->getHidden()) { + return \in_array($attribute, $hidden, true); + } + + return false; + } + + /** + * Determines if the given attribute is unique. + * + * @param array $indexes + */ + private function columnIsUnique(string $column, array $indexes): bool + { + return collect($indexes)->contains( + fn ($index) => 1 === \count($index['columns']) && $index['columns'][0] === $column && $index['unique'] + ); + } +} diff --git a/src/Laravel/Eloquent/Metadata/ResourceClassResolver.php b/src/Laravel/Eloquent/Metadata/ResourceClassResolver.php new file mode 100644 index 00000000000..69aa2f948b0 --- /dev/null +++ b/src/Laravel/Eloquent/Metadata/ResourceClassResolver.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Metadata; + +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use Illuminate\Database\Eloquent\Relations\Relation; + +final class ResourceClassResolver implements ResourceClassResolverInterface +{ + public function __construct( + private readonly ResourceClassResolverInterface $inner, + ) { + } + + public function getResourceClass(mixed $value, ?string $resourceClass = null, bool $strict = false): string + { + if ($value instanceof Relation) { + return $this->inner->getResourceClass($value->getRelated()); + } + + return $this->inner->getResourceClass($value, $resourceClass, $strict); + } + + public function isResourceClass(string $type): bool + { + return $this->inner->isResourceClass($type); + } +} diff --git a/src/Laravel/Eloquent/Paginator.php b/src/Laravel/Eloquent/Paginator.php new file mode 100644 index 00000000000..4085cb18255 --- /dev/null +++ b/src/Laravel/Eloquent/Paginator.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent; + +use ApiPlatform\State\Pagination\HasNextPagePaginatorInterface; +use ApiPlatform\State\Pagination\PaginatorInterface; +use Illuminate\Pagination\LengthAwarePaginator; + +/** + * @implements \IteratorAggregate + * @implements PaginatorInterface + */ +final class Paginator implements PaginatorInterface, HasNextPagePaginatorInterface, \IteratorAggregate +{ + /** + * @param LengthAwarePaginator $paginator + */ + public function __construct( + private readonly LengthAwarePaginator $paginator, + ) { + } + + public function count(): int + { + return $this->paginator->count(); // @phpstan-ignore-line + } + + public function getLastPage(): float + { + return $this->paginator->lastPage(); + } + + public function getTotalItems(): float + { + return $this->paginator->total(); + } + + public function getCurrentPage(): float + { + return $this->paginator->currentPage(); + } + + public function getItemsPerPage(): float + { + return $this->paginator->perPage(); + } + + public function getIterator(): \Traversable + { + return $this->paginator->getIterator(); + } + + public function hasNextPage(): bool + { + return $this->paginator->hasMorePages(); + } +} diff --git a/src/Laravel/Eloquent/PartialPaginator.php b/src/Laravel/Eloquent/PartialPaginator.php new file mode 100644 index 00000000000..004cb228ab1 --- /dev/null +++ b/src/Laravel/Eloquent/PartialPaginator.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent; + +use ApiPlatform\State\Pagination\PartialPaginatorInterface; +use Illuminate\Pagination\AbstractPaginator; + +/** + * @implements \IteratorAggregate + * @implements PartialPaginatorInterface + */ +final class PartialPaginator implements PartialPaginatorInterface, \IteratorAggregate +{ + /** + * @param AbstractPaginator $paginator + */ + public function __construct( + private readonly AbstractPaginator $paginator, + ) { + } + + public function count(): int + { + return $this->paginator->count(); // @phpstan-ignore-line + } + + public function getCurrentPage(): float + { + return $this->paginator->currentPage(); + } + + public function getItemsPerPage(): float + { + return $this->paginator->perPage(); + } + + public function getIterator(): \Traversable + { + return $this->paginator->getIterator(); + } +} diff --git a/src/Laravel/Eloquent/PropertyAccess/PropertyAccessor.php b/src/Laravel/Eloquent/PropertyAccess/PropertyAccessor.php new file mode 100644 index 00000000000..0c07bbb0adf --- /dev/null +++ b/src/Laravel/Eloquent/PropertyAccess/PropertyAccessor.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\PropertyAccess; + +use Illuminate\Database\Eloquent\Model; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyPathInterface; + +/** + * @internal + */ +final class PropertyAccessor implements PropertyAccessorInterface +{ + private readonly PropertyAccessorInterface $inner; + + public function __construct( + ?PropertyAccessorInterface $inner = null, + ) { + $this->inner = $inner ?? PropertyAccess::createPropertyAccessor(); + } + + /** + * @param object|array $objectOrArray + * + * @param-out object|array $objectOrArray + */ + public function setValue(object|array &$objectOrArray, string|PropertyPathInterface $propertyPath, mixed $value): void + { + if ($objectOrArray instanceof Model) { + $objectOrArray->{$propertyPath} = $value; + + return; + } + + $this->inner->setValue($objectOrArray, $propertyPath, $value); + } + + /** + * @param array|object $objectOrArray + */ + public function getValue(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): mixed + { + if ($objectOrArray instanceof Model) { + return $objectOrArray->{$propertyPath}; + } + + return $this->inner->getValue($objectOrArray, $propertyPath); + } + + /** + * @param array|object $objectOrArray + */ + public function isWritable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool + { + if ($objectOrArray instanceof Model) { + return true; + } + + return $this->inner->isWritable($objectOrArray, $propertyPath); + } + + /** + * @param array|object $objectOrArray + */ + public function isReadable(object|array $objectOrArray, string|PropertyPathInterface $propertyPath): bool + { + if ($objectOrArray instanceof Model) { + return true; + } + + return $this->inner->isReadable($objectOrArray, $propertyPath); + } +} diff --git a/src/Laravel/Eloquent/PropertyInfo/EloquentExtractor.php b/src/Laravel/Eloquent/PropertyInfo/EloquentExtractor.php new file mode 100644 index 00000000000..ca2fd64b654 --- /dev/null +++ b/src/Laravel/Eloquent/PropertyInfo/EloquentExtractor.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\PropertyInfo; + +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use Illuminate\Database\Eloquent\Model; +use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; + +class EloquentExtractor implements PropertyAccessExtractorInterface +{ + public function __construct(private readonly ModelMetadata $modelMetadata) + { + } + + /** + * @param array $context + */ + public function isReadable(string $class, string $property, array $context = []): ?bool + { + if (!is_a($class, Model::class, true)) { + return null; + } + + try { + $refl = new \ReflectionClass($class); + $model = $refl->newInstanceWithoutConstructor(); + } catch (\ReflectionException) { + return null; + } + + foreach ($this->modelMetadata->getAttributes($model) as $p) { + if ($p['name'] !== $property) { + continue; + } + + if (($visible = $model->getVisible()) && \in_array($property, $visible, true)) { + return true; + } + + if (($hidden = $model->getHidden()) && \in_array($property, $hidden, true)) { + return false; + } + + return true; + } + + return null; + } + + /** + * @param array $context + */ + public function isWritable(string $class, string $property, array $context = []): ?bool + { + if (!is_a($class, Model::class, true)) { + return null; + } + + try { + $refl = new \ReflectionClass($class); + $model = $refl->newInstanceWithoutConstructor(); + } catch (\ReflectionException) { + return null; + } + + foreach ($this->modelMetadata->getAttributes($model) as $p) { + if ($p['name'] !== $property) { + continue; + } + + if ($fillable = $model->getFillable()) { + return \in_array($property, $fillable, true); + } + + return true; + } + + return null; + } +} diff --git a/src/Laravel/Eloquent/Serializer/EloquentNameConverter.php b/src/Laravel/Eloquent/Serializer/EloquentNameConverter.php new file mode 100644 index 00000000000..67fd8aa7749 --- /dev/null +++ b/src/Laravel/Eloquent/Serializer/EloquentNameConverter.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Serializer; + +use Symfony\Component\Serializer\Exception\UnexpectedPropertyException; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +final class EloquentNameConverter implements NameConverterInterface +{ + public function __construct(private readonly NameConverterInterface $nameConverter) + { + } + + /** + * @param array $context + */ + public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + try { + return $this->nameConverter->normalize($propertyName, $class, $format, $context); // @phpstan-ignore-line + } catch (UnexpectedPropertyException $e) { + return $this->nameConverter->denormalize($propertyName, $class, $format, $context); // @phpstan-ignore-line + } + } + + /** + * @param array $context + */ + public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string + { + try { + return $this->nameConverter->denormalize($propertyName, $class, $format, $context); // @phpstan-ignore-line + } catch (UnexpectedPropertyException $e) { + return $this->nameConverter->normalize($propertyName, $class, $format, $context); // @phpstan-ignore-line + } + } +} diff --git a/src/Laravel/Eloquent/Serializer/SerializerClassMetadataFactory.php.bak b/src/Laravel/Eloquent/Serializer/SerializerClassMetadataFactory.php.bak new file mode 100644 index 00000000000..60d80d34f62 --- /dev/null +++ b/src/Laravel/Eloquent/Serializer/SerializerClassMetadataFactory.php.bak @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Serializer; + +use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; + +class SerializerClassMetadataFactory implements ClassMetadataFactoryInterface +{ + public function __construct(private readonly ClassMetadataFactoryInterface $decorated) + { + } + + /** + * {@inheritdoc} + */ + public function getMetadataFor($value): ClassMetadataInterface + { + return $this->decorated->getMetadataFor($value); + } + + /** + * {@inheritdoc} + */ + public function hasMetadataFor(mixed $value): bool + { + return $this->decorated->hasMetadataFor($value); + } +} diff --git a/src/Laravel/Eloquent/Serializer/SerializerContextBuilder.php b/src/Laravel/Eloquent/Serializer/SerializerContextBuilder.php new file mode 100644 index 00000000000..7f4685c043b --- /dev/null +++ b/src/Laravel/Eloquent/Serializer/SerializerContextBuilder.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\Serializer; + +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\State\SerializerContextBuilderInterface; +use Illuminate\Database\Eloquent\Model; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; +use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + +final class SerializerContextBuilder implements SerializerContextBuilderInterface +{ + /** + * @param class-string $nameConverterClass + */ + public function __construct( + private readonly SerializerContextBuilderInterface $decorated, + private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, + private readonly ?string $nameConverterClass = null, + ) { + } + + /** + * @param array $extractedAttributes + */ + public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array + { + $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); + if (!isset($context['resource_class']) || !\is_string($context['resource_class']) || !is_a($context['resource_class'], Model::class, true)) { + return $context; + } + + if (SnakeCaseToCamelCaseNameConverter::class === $this->nameConverterClass) { + $context[SnakeCaseToCamelCaseNameConverter::REQUIRE_CAMEL_CASE_PROPERTIES] = true; + } elseif (CamelCaseToSnakeCaseNameConverter::class === $this->nameConverterClass) { + $context[CamelCaseToSnakeCaseNameConverter::REQUIRE_SNAKE_CASE_PROPERTIES] = true; + } + + if (!isset($context[AbstractNormalizer::ATTRIBUTES])) { + // isWritable/isReadable is checked later on + $context[AbstractNormalizer::ATTRIBUTES] = iterator_to_array($this->propertyNameCollectionFactory->create($context['resource_class'], ['serializer_groups' => $context['groups'] ?? null])); + } + + return $context; + } +} diff --git a/src/Laravel/Eloquent/State/CollectionProvider.php b/src/Laravel/Eloquent/State/CollectionProvider.php new file mode 100644 index 00000000000..effa3e400dd --- /dev/null +++ b/src/Laravel/Eloquent/State/CollectionProvider.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface; +use ApiPlatform\Laravel\Eloquent\Paginator; +use ApiPlatform\Laravel\Eloquent\PartialPaginator; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\Pagination\Pagination; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\Util\StateOptionsTrait; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Model; +use Psr\Container\ContainerInterface; + +/** + * @implements ProviderInterface|PartialPaginator> + */ +final class CollectionProvider implements ProviderInterface +{ + use LinksHandlerLocatorTrait; + use StateOptionsTrait; + + /** + * @param LinksHandlerInterface $linksHandler + * @param iterable $queryExtensions + */ + public function __construct( + private readonly Pagination $pagination, + private readonly LinksHandlerInterface $linksHandler, + private iterable $queryExtensions = [], + ?ContainerInterface $handleLinksLocator = null, + ) { + $this->handleLinksLocator = $handleLinksLocator; + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $resourceClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $model = new $resourceClass(); + + if (!$model instanceof Model) { + throw new RuntimeException(\sprintf('The class "%s" is not an Eloquent model.', $resourceClass)); + } + + if ($handleLinks = $this->getLinksHandler($operation)) { + $query = $handleLinks($model->query(), $uriVariables, ['operation' => $operation, 'modelClass' => $operation->getClass()] + $context); + } else { + $query = $this->linksHandler->handleLinks($model->query(), $uriVariables, ['operation' => $operation, 'modelClass' => $operation->getClass()] + $context); + } + + foreach ($this->queryExtensions as $extension) { + $query = $extension->apply($query, $uriVariables, $operation, $context); + } + + if ($order = $operation->getOrder()) { + $isList = array_is_list($order); + foreach ($order as $property => $direction) { + if ($isList) { + $property = $direction; + $direction = 'ASC'; + } + + if (str_contains($property, '.')) { + [$table, $property] = explode('.', $property); + + // Relation Order by, we need to do laravel eager loading + $query->with([ + $table => fn ($query) => $query->orderBy($property, $direction), + ]); + + continue; + } + + $query->orderBy($property, $direction); + } + } + + if (false === $this->pagination->isEnabled($operation, $context)) { + return $query->get(); + } + + $isPartial = $operation->getPaginationPartial(); + $collection = $query + ->{$isPartial ? 'simplePaginate' : 'paginate'}( + perPage: $this->pagination->getLimit($operation, $context), + page: $this->pagination->getPage($context), + ); + + if ($isPartial) { + return new PartialPaginator($collection); + } + + return new Paginator($collection); + } +} diff --git a/src/Laravel/Eloquent/State/ItemProvider.php b/src/Laravel/Eloquent/State/ItemProvider.php new file mode 100644 index 00000000000..38d2bb1bbb5 --- /dev/null +++ b/src/Laravel/Eloquent/State/ItemProvider.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\Util\StateOptionsTrait; +use Illuminate\Database\Eloquent\Model; +use Psr\Container\ContainerInterface; + +/** + * @implements ProviderInterface + */ +final class ItemProvider implements ProviderInterface +{ + use LinksHandlerLocatorTrait; + use StateOptionsTrait; + + /** + * @param LinksHandlerInterface $linksHandler + * @param iterable $queryExtensions + */ + public function __construct( + private readonly LinksHandlerInterface $linksHandler, + ?ContainerInterface $handleLinksLocator = null, + private iterable $queryExtensions = [], + ) { + $this->handleLinksLocator = $handleLinksLocator; + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $resourceClass = $this->getStateOptionsClass($operation, $operation->getClass(), Options::class); + $model = new $resourceClass(); + + if (!$model instanceof Model) { + throw new RuntimeException(\sprintf('The class "%s" is not an Eloquent model.', $resourceClass)); + } + + if ($handleLinks = $this->getLinksHandler($operation)) { + $query = $handleLinks($model->query(), $uriVariables, ['operation' => $operation] + $context); + } else { + $query = $this->linksHandler->handleLinks($model->query(), $uriVariables, ['operation' => $operation] + $context); + } + + foreach ($this->queryExtensions as $extension) { + $query = $extension->apply($query, $uriVariables, $operation, $context); + } + + return $query->first(); + } +} diff --git a/src/Laravel/Eloquent/State/LinksHandler.php b/src/Laravel/Eloquent/State/LinksHandler.php new file mode 100644 index 00000000000..8eec2a304d3 --- /dev/null +++ b/src/Laravel/Eloquent/State/LinksHandler.php @@ -0,0 +1,167 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Metadata\Exception\OperationNotFoundException; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\GraphQl\Operation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\BelongsToMany; +use Illuminate\Database\Eloquent\Relations\HasOneOrMany; +use Illuminate\Database\Eloquent\Relations\MorphTo; +use Illuminate\Database\Eloquent\Relations\Relation; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; + +/** + * @implements LinksHandlerInterface + */ +final class LinksHandler implements LinksHandlerInterface +{ + public function __construct( + private readonly Application $application, + private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, + ) { + } + + public function handleLinks(Builder $builder, array $uriVariables, array $context): Builder + { + $operation = $context['operation']; + + if ($operation instanceof HttpOperation) { + foreach (array_reverse($operation->getUriVariables() ?? []) as $uriVariable => $link) { + if (isset($uriVariables[$uriVariable])) { + $builder = $this->buildQuery($builder, $link, $uriVariables[$uriVariable]); + } + } + + return $builder; + } + + if (!($linkClass = $context['linkClass'] ?? false)) { + return $builder; + } + + $newLink = null; + $linkedOperation = null; + $linkProperty = $context['linkProperty'] ?? null; + + try { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($linkClass); + $linkedOperation = $resourceMetadataCollection->getOperation($operation->getName()); + } catch (OperationNotFoundException) { + // Instead, we'll look for the first Query available. + foreach ($resourceMetadataCollection as $resourceMetadata) { + foreach ($resourceMetadata->getGraphQlOperations() as $op) { + if ($op instanceof Query) { + $linkedOperation = $op; + } + } + } + } + + if (!$linkedOperation instanceof Operation) { + return $builder; + } + + $resourceClass = $builder->getModel()::class; + foreach ($linkedOperation->getLinks() ?? [] as $link) { + if ($resourceClass === $link->getToClass() && $linkProperty === $link->getFromProperty()) { + $newLink = $link; + break; + } + } + + if (!$newLink) { + return $builder; + } + + return $this->buildQuery($builder, $newLink, $uriVariables[$newLink->getIdentifiers()[0]]); + } + + /** + * @param Builder $builder + * + * @throws \Illuminate\Contracts\Container\BindingResolutionException + * + * @return Builder $builder + */ + private function buildQuery(Builder $builder, Link $link, mixed $identifier): Builder + { + if ($to = $link->getToProperty()) { + return $builder->where($builder->getModel()->{$to}()->getQualifiedForeignKeyName(), $identifier); + } + + if ($from = $link->getFromProperty()) { + /** @var Model $relatedInstance */ + $relatedInstance = $this->application->make($link->getFromClass()); + + $identifierField = $link->getIdentifiers()[0]; + + if ($identifierField !== $relatedInstance->getKeyName()) { + $relatedInstance = $relatedInstance + ->newQuery() + ->where($identifierField, $identifier) + ->first(); + } else { + $relatedInstance->setAttribute($identifierField, $identifier); + $relatedInstance->exists = true; + } + + if (!$relatedInstance) { + throw new NotFoundHttpException('Not Found'); + } + + /** @var Relation $relation */ + $relation = $relatedInstance->{$from}(); + + if ($relation instanceof MorphTo) { + throw new RuntimeException('Cannot query directly from a MorphTo relationship.'); + } + + if ($relation instanceof BelongsTo) { + return $builder->getModel() + ->join( + $relation->getParent()->getTable(), + $relation->getParent()->getQualifiedKeyName(), + $identifier + ); + } + + if ($relation instanceof HasOneOrMany || $relation instanceof BelongsToMany) { + return $relation->getQuery(); + } + + if (method_exists($relation, 'getQualifiedForeignKeyName')) { + return $relation->getQuery()->where( + $relation->getQualifiedForeignKeyName(), + $identifier + ); + } + + throw new RuntimeException(\sprintf('Unhandled or unknown relationship type: %s for property %s on %s', $relation::class, $from, $relatedInstance::class)); + } + + return $builder->where( + $builder->getModel()->qualifyColumn($link->getIdentifiers()[0]), + $identifier + ); + } +} diff --git a/src/Laravel/Eloquent/State/LinksHandlerInterface.php b/src/Laravel/Eloquent/State/LinksHandlerInterface.php new file mode 100644 index 00000000000..ae3089e501c --- /dev/null +++ b/src/Laravel/Eloquent/State/LinksHandlerInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Metadata\Operation; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; + +/** + * @template T of Model + */ +interface LinksHandlerInterface +{ + /** + * Handles Laravel links. + * + * @param Builder $builder + * @param array $uriVariables + * @param array{modelClass: string, operation: Operation}|array $context + * + * @return Builder + */ + public function handleLinks(Builder $builder, array $uriVariables, array $context): Builder; +} diff --git a/src/Laravel/Eloquent/State/LinksHandlerLocatorTrait.php b/src/Laravel/Eloquent/State/LinksHandlerLocatorTrait.php new file mode 100644 index 00000000000..c608a854b22 --- /dev/null +++ b/src/Laravel/Eloquent/State/LinksHandlerLocatorTrait.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; +use Psr\Container\ContainerInterface; + +/** + * @internal + */ +trait LinksHandlerLocatorTrait +{ + private ?ContainerInterface $handleLinksLocator; + + private function getLinksHandler(Operation $operation): ?callable + { + if (!($options = $operation->getStateOptions()) || !$options instanceof Options || !$options->getHandleLinks()) { + return null; + } + + $handleLinks = $options->getHandleLinks(); + if (\is_callable($handleLinks)) { + return $handleLinks; + } + + if ($this->handleLinksLocator && \is_string($handleLinks) && $this->handleLinksLocator->has($handleLinks)) { + return [$this->handleLinksLocator->get($handleLinks), 'handleLinks']; // @phpstan-ignore-line + } + + throw new RuntimeException(\sprintf('Could not find handleLinks service "%s"', $handleLinks)); + } +} diff --git a/src/Laravel/Eloquent/State/Options.php b/src/Laravel/Eloquent/State/Options.php new file mode 100644 index 00000000000..3e328adf3d2 --- /dev/null +++ b/src/Laravel/Eloquent/State/Options.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\State\OptionsInterface; + +class Options implements OptionsInterface +{ + /** + * @param string|callable $handleLinks experimental callable, typed mixed as we may want a service name in the future + * + * @see LinksHandlerInterface + */ + public function __construct( + protected ?string $modelClass = null, + protected mixed $handleLinks = null, + ) { + } + + public function getHandleLinks(): mixed + { + return $this->handleLinks; + } + + public function withHandleLinks(mixed $handleLinks): self + { + $self = clone $this; + $self->handleLinks = $handleLinks; + + return $self; + } + + public function getModelClass(): ?string + { + return $this->modelClass; + } + + public function withModelClass(?string $modelClass): self + { + $self = clone $this; + $self->modelClass = $modelClass; + + return $self; + } +} diff --git a/src/Laravel/Eloquent/State/PersistProcessor.php b/src/Laravel/Eloquent/State/PersistProcessor.php new file mode 100644 index 00000000000..b1298a2ddaa --- /dev/null +++ b/src/Laravel/Eloquent/State/PersistProcessor.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\MorphMany; +use Illuminate\Database\Eloquent\Relations\MorphTo; + +/** + * @implements ProcessorInterface<\Illuminate\Database\Eloquent\Model, \Illuminate\Database\Eloquent\Model> + */ +final class PersistProcessor implements ProcessorInterface +{ + /** + * @var array + */ + private array $relations; + + public function __construct( + private readonly ModelMetadata $modelMetadata, + ) { + } + + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) + { + $toMany = []; + + foreach ($this->modelMetadata->getRelations($data) as $relation) { + if (!isset($data->{$relation['name']})) { + continue; + } + + if (BelongsTo::class === $relation['type'] || MorphTo::class === $relation['type']) { + $rel = $data->{$relation['name']}; + + if (!$rel->exists) { + $rel->save(); + } + + $data->{$relation['method_name']}()->associate($data->{$relation['name']}); + unset($data->{$relation['name']}); + $this->relations[$relation['method_name']] = $relation['name']; + } + + if (HasMany::class === $relation['type'] || MorphMany::class === $relation['type']) { + $rel = $data->{$relation['name']}; + + if (!\is_array($rel) && !$rel instanceof Collection) { + throw new RuntimeException('To-Many relationship is not a collection.'); + } + + $toMany[$relation['method_name']] = $rel; + unset($data->{$relation['name']}); + $this->relations[$relation['method_name']] = $relation['name']; + } + } + + if (($previousData = $context['previous_data'] ?? null) && $operation instanceof HttpOperation && 'PUT' === $operation->getMethod() && ($operation->getExtraProperties()['standard_put'] ?? true)) { + foreach ($this->modelMetadata->getAttributes($data) as $attribute) { + if ($attribute['primary'] ?? false) { + $data->{$attribute['name']} = $previousData->{$attribute['name']}; + } + } + $data->exists = true; + } + + $data->saveOrFail(); + $data->refresh(); + + foreach ($data->getRelations() as $methodName => $obj) { + if (isset($this->relations[$methodName])) { + $data->{$this->relations[$methodName]} = $obj; + } + } + + foreach ($toMany as $methodName => $relations) { + $data->{$methodName}()->saveMany($relations); + $data->{$this->relations[$methodName]} = $relations; + unset($toMany[$methodName]); + } + + return $data; + } +} diff --git a/src/Laravel/Eloquent/State/RemoveProcessor.php b/src/Laravel/Eloquent/State/RemoveProcessor.php new file mode 100644 index 00000000000..a4a4583a01b --- /dev/null +++ b/src/Laravel/Eloquent/State/RemoveProcessor.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Eloquent\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProcessorInterface; + +/** + * @implements ProcessorInterface<\Illuminate\Database\Eloquent\Model, null> + */ +final class RemoveProcessor implements ProcessorInterface +{ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) + { + $data->delete(); + + return null; + } +} diff --git a/src/Laravel/Exception/ErrorHandler.php b/src/Laravel/Exception/ErrorHandler.php new file mode 100644 index 00000000000..a0796b9843c --- /dev/null +++ b/src/Laravel/Exception/ErrorHandler.php @@ -0,0 +1,232 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Exception; + +use ApiPlatform\Laravel\ApiResource\Error; +use ApiPlatform\Laravel\Controller\ApiPlatformController; +use ApiPlatform\Metadata\Exception\InvalidUriVariableException; +use ApiPlatform\Metadata\Exception\ProblemExceptionInterface; +use ApiPlatform\Metadata\Exception\StatusAwareExceptionInterface; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\Util\ContentNegotiationTrait; +use ApiPlatform\State\Util\OperationRequestInitiatorTrait; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Auth\AuthenticationException; +use Illuminate\Contracts\Container\Container; +use Illuminate\Contracts\Debug\ExceptionHandler; +use Illuminate\Foundation\Exceptions\Handler as ExceptionsHandler; +use Negotiation\Negotiator; +use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface as SymfonyHttpExceptionInterface; +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; + +class ErrorHandler extends ExceptionsHandler +{ + use ContentNegotiationTrait; + use OperationRequestInitiatorTrait; + + public static mixed $error; + + /** + * @param array $exceptionToStatus + * @param array $errorFormats + */ + public function __construct( + Container $container, + ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, + private readonly ApiPlatformController $apiPlatformController, + private readonly ?IdentifiersExtractorInterface $identifiersExtractor = null, + private readonly ?ResourceClassResolverInterface $resourceClassResolver = null, + ?Negotiator $negotiator = null, + private readonly ?array $exceptionToStatus = null, + private readonly ?bool $debug = false, + private readonly ?array $errorFormats = null, + private readonly ?ExceptionHandler $decorated = null, + ) { + $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; + $this->negotiator = $negotiator; + parent::__construct($container); + } + + public function render($request, \Throwable $exception) + { + $apiOperation = $this->initializeOperation($request); + + if (!$apiOperation) { + return $this->decorated ? $this->decorated->render($request, $exception) : parent::render($request, $exception); + } + + $formats = $this->errorFormats ?? ['jsonproblem' => ['application/problem+json']]; + $format = $request->getRequestFormat() ?? $this->getRequestFormat($request, $formats, false); + + if ($this->resourceClassResolver->isResourceClass($exception::class)) { + $resourceCollection = $this->resourceMetadataCollectionFactory->create($exception::class); + + $operation = null; + foreach ($resourceCollection as $resource) { + foreach ($resource->getOperations() as $op) { + foreach ($op->getOutputFormats() ?? [] as $key => $value) { + if ($key === $format) { + $operation = $op; + break 3; + } + } + } + } + + // No operation found for the requested format, we take the first available + if (!$operation) { + $operation = $resourceCollection->getOperation(); + } + $errorResource = $exception; + if ($errorResource instanceof ProblemExceptionInterface && $operation instanceof HttpOperation) { + $statusCode = $this->getStatusCode($apiOperation, $operation, $exception); + $operation = $operation->withStatus($statusCode); + if ($errorResource instanceof StatusAwareExceptionInterface) { + $errorResource->setStatus($statusCode); + } + } + } else { + // Create a generic, rfc7807 compatible error according to the wanted format + $operation = $this->resourceMetadataCollectionFactory->create(Error::class)->getOperation($this->getFormatOperation($format)); + // status code may be overridden by the exceptionToStatus option + $statusCode = 500; + if ($operation instanceof HttpOperation) { + $statusCode = $this->getStatusCode($apiOperation, $operation, $exception); + $operation = $operation->withStatus($statusCode); + } + + $errorResource = Error::createFromException($exception, $statusCode); + } + + /** @var HttpOperation $operation */ + if (!$operation->getProvider()) { + static::$error = $errorResource; + $operation = $operation->withProvider([self::class, 'provide']); + } + + // For our swagger Ui errors + if ('html' === $format) { + $operation = $operation->withOutputFormats(['html' => ['text/html']]); + } + + $identifiers = []; + try { + $identifiers = $this->identifiersExtractor?->getIdentifiersFromItem($errorResource, $operation) ?? []; + } catch (\Exception $e) { + } + + $normalizationContext = $operation->getNormalizationContext() ?? []; + if (!($normalizationContext['api_error_resource'] ?? false)) { + $normalizationContext += ['api_error_resource' => true]; + } + + if (!isset($normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES])) { + $normalizationContext[AbstractObjectNormalizer::IGNORED_ATTRIBUTES] = true === $this->debug ? [] : ['originalTrace']; + } + + $operation = $operation->withNormalizationContext($normalizationContext); + + $dup = $request->duplicate(null, null, []); + $dup->setMethod('GET'); + $dup->attributes->set('_api_resource_class', $operation->getClass()); + $dup->attributes->set('_api_previous_operation', $apiOperation); + $dup->attributes->set('_api_operation', $operation); + $dup->attributes->set('_api_operation_name', $operation->getName()); + $dup->attributes->set('exception', $exception); + // These are for swagger + $dup->attributes->set('_api_original_route', $request->attributes->get('_route')); + $dup->attributes->set('_api_original_uri_variables', $request->attributes->get('_api_uri_variables')); + $dup->attributes->set('_api_original_route_params', $request->attributes->get('_route_params')); + $dup->attributes->set('_api_requested_operation', $request->attributes->get('_api_requested_operation')); + + foreach ($identifiers as $name => $value) { + $dup->attributes->set($name, $value); + } + + try { + $response = $this->apiPlatformController->__invoke($dup); + $this->decorated->render($dup, $exception); + + return $response; + } catch (\Throwable $e) { + return $this->decorated ? $this->decorated->render($request, $exception) : parent::render($request, $exception); + } + } + + private function getStatusCode(?HttpOperation $apiOperation, ?HttpOperation $errorOperation, \Throwable $exception): int + { + $exceptionToStatus = array_merge( + $apiOperation ? $apiOperation->getExceptionToStatus() ?? [] : [], + $errorOperation ? $errorOperation->getExceptionToStatus() ?? [] : [], + $this->exceptionToStatus ?? [] + ); + + foreach ($exceptionToStatus as $class => $status) { + if (is_a($exception::class, $class, true)) { + return $status; + } + } + + if ($exception instanceof AuthenticationException) { + return 401; + } + + if ($exception instanceof AuthorizationException) { + return 403; + } + + if ($exception instanceof SymfonyHttpExceptionInterface) { + return $exception->getStatusCode(); + } + + if ($exception instanceof RequestExceptionInterface || $exception instanceof InvalidUriVariableException) { + return 400; + } + + // if ($exception instanceof ValidationException) { + // return 422; + // } + + if ($status = $errorOperation?->getStatus()) { + return $status; + } + + return 500; + } + + private function getFormatOperation(?string $format): string + { + return match ($format) { + 'json' => '_api_errors_problem', + 'jsonproblem' => '_api_errors_problem', + 'jsonld' => '_api_errors_hydra', + 'jsonapi' => '_api_errors_jsonapi', + 'html' => '_api_errors_problem', // This will be intercepted by the SwaggerUiProvider + default => '_api_errors_problem', + }; + } + + public static function provide(): mixed + { + if ($data = static::$error) { + return $data; + } + + throw new \LogicException(\sprintf('We could not find the thrown exception in the %s.', self::class)); + } +} diff --git a/src/Laravel/GraphQl/Controller/EntrypointController.php b/src/Laravel/GraphQl/Controller/EntrypointController.php new file mode 100644 index 00000000000..9d47eb514e4 --- /dev/null +++ b/src/Laravel/GraphQl/Controller/EntrypointController.php @@ -0,0 +1,230 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\GraphQl\Controller; + +use ApiPlatform\GraphQl\Error\ErrorHandlerInterface; +use ApiPlatform\GraphQl\ExecutorInterface; +use ApiPlatform\GraphQl\Type\SchemaBuilderInterface; +use ApiPlatform\Metadata\Util\ContentNegotiationTrait; +use GraphQL\Error\DebugFlag; +use GraphQL\Error\Error; +use GraphQL\Executor\ExecutionResult; +use Illuminate\Http\Request; +use Negotiation\Negotiator; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +final class EntrypointController +{ + use ContentNegotiationTrait; + private int $debug; + + /** + * @param array $formats + */ + public function __construct( + private readonly SchemaBuilderInterface $schemaBuilder, + private readonly ExecutorInterface $executor, + private readonly GraphiQlController $graphiQlAction, + private readonly NormalizerInterface $normalizer, + private readonly ErrorHandlerInterface $errorHandler, + bool $debug = false, + ?Negotiator $negotiator = null, + private readonly array $formats = [], + ) { + $this->debug = $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE; + $this->negotiator = $negotiator ?? new Negotiator(); + } + + public function __invoke(Request $request): Response + { + $formats = ['json' => ['application/json'], 'html' => ['text/html']]; + + foreach ($this->formats as $k => $f) { + if (!isset($formats[$k])) { + $formats[$k] = $f; + } + } + + $this->addRequestFormats($request, $formats); + $format = $this->getRequestFormat($request, $formats, false); + $request->setRequestFormat($format); + + try { + if ($request->isMethod('GET') && 'html' === $format) { + return ($this->graphiQlAction)(); + } + + [$query, $operationName, $variables] = $this->parseRequest($request, $format); + if (null === $query) { + throw new BadRequestHttpException('GraphQL query is not valid.'); + } + + $executionResult = $this->executor + ->executeQuery($this->schemaBuilder->getSchema(), $query, null, null, $variables, $operationName) + ->setErrorsHandler($this->errorHandler) + ->setErrorFormatter($this->normalizer->normalize(...)); + } catch (\Exception $exception) { + $executionResult = (new ExecutionResult(null, [new Error($exception->getMessage(), null, null, [], null, $exception)])) + ->setErrorsHandler($this->errorHandler) + ->setErrorFormatter($this->normalizer->normalize(...)); + } + + return new JsonResponse($executionResult->toArray($this->debug)); + } + + /** + * @throws BadRequestHttpException + * + * @return array{0: array|null, 1: string, 2: array} + */ + private function parseRequest(Request $request, string $format): array + { + $queryParameters = $request->query->all(); + $query = $queryParameters['query'] ?? null; + $operationName = $queryParameters['operationName'] ?? null; + if ($variables = $queryParameters['variables'] ?? []) { + $variables = $this->decodeVariables($variables); + } + + if (!$request->isMethod('POST')) { + return [$query, $operationName, $variables]; + } + + if ('json' === $format) { + return $this->parseData($query, $operationName, $variables, $request->getContent()); + } + + if ('graphql' === $format) { + $query = $request->getContent(); + } + + if ('multipart' === $format) { + return $this->parseMultipartRequest($query, $operationName, $variables, $request->request->all(), $request->files->all()); + } + + return [$query, $operationName, $variables]; + } + + /** + * @param array $variables + * + * @throws BadRequestHttpException + * + * @return array{0: array, 1: string, 2: array} + */ + private function parseData(?string $query, ?string $operationName, array $variables, string $jsonContent): array + { + if (!\is_array($data = json_decode($jsonContent, true, 512, \JSON_ERROR_NONE))) { + throw new BadRequestHttpException('GraphQL data is not valid JSON.'); + } + + if (isset($data['query'])) { + $query = $data['query']; + } + + if (isset($data['variables'])) { + $variables = \is_array($data['variables']) ? $data['variables'] : $this->decodeVariables($data['variables']); + } + + if (isset($data['operationName'])) { + $operationName = $data['operationName']; + } + + return [$query, $operationName, $variables]; + } + + /** + * @param array $variables + * @param array $bodyParameters + * @param array $files + * + * @throws BadRequestHttpException + * + * @return array{0: array, 1: string, 2: array} + */ + private function parseMultipartRequest(?string $query, ?string $operationName, array $variables, array $bodyParameters, array $files): array + { + if ((null === $operations = $bodyParameters['operations'] ?? null) || (null === $map = $bodyParameters['map'] ?? null)) { + throw new BadRequestHttpException('GraphQL multipart request does not respect the specification.'); + } + + [$query, $operationName, $variables] = $this->parseData($query, $operationName, $variables, $operations); + + /** @var string $map */ + if (!\is_array($decodedMap = json_decode($map, true, 512, \JSON_ERROR_NONE))) { + throw new BadRequestHttpException('GraphQL multipart request map is not valid JSON.'); + } + + $variables = $this->applyMapToVariables($decodedMap, $variables, $files); + + return [$query, $operationName, $variables]; + } + + /** + * @param array $map + * @param array $variables + * @param array $files + * + * @throws BadRequestHttpException + */ + private function applyMapToVariables(array $map, array $variables, array $files): array + { + foreach ($map as $key => $value) { + if (null === $file = $files[$key] ?? null) { + throw new BadRequestHttpException('GraphQL multipart request file has not been sent correctly.'); + } + + foreach ($value as $mapValue) { + $path = explode('.', (string) $mapValue); + + if ('variables' !== $path[0]) { + throw new BadRequestHttpException('GraphQL multipart request path in map is invalid.'); + } + + unset($path[0]); + + $mapPathExistsInVariables = array_reduce($path, static fn (array $inVariables, string $pathElement) => \array_key_exists($pathElement, $inVariables) ? $inVariables[$pathElement] : false, $variables); + + if (false === $mapPathExistsInVariables) { + throw new BadRequestHttpException('GraphQL multipart request path in map does not match the variables.'); + } + + $variableFileValue = &$variables; + foreach ($path as $pathValue) { + $variableFileValue = &$variableFileValue[$pathValue]; + } + $variableFileValue = $file; + } + } + + return $variables; + } + + /** + * @throws BadRequestHttpException + * + * @return array + */ + private function decodeVariables(string $variables): array + { + if (!\is_array($decoded = json_decode($variables, true, 512, \JSON_ERROR_NONE))) { + throw new BadRequestHttpException('GraphQL variables are not valid JSON.'); + } + + return $decoded; + } +} diff --git a/src/Laravel/GraphQl/Controller/GraphiQlController.php b/src/Laravel/GraphQl/Controller/GraphiQlController.php new file mode 100644 index 00000000000..514d7a337e2 --- /dev/null +++ b/src/Laravel/GraphQl/Controller/GraphiQlController.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\GraphQl\Controller; + +use Illuminate\Http\Response; + +readonly class GraphiQlController +{ + public function __construct(private readonly string $prefix) + { + } + + public function __invoke(): Response + { + return new Response(view('api-platform::graphiql', ['graphiql_data' => ['entrypoint' => $this->prefix.'/graphql']]), 200); + } +} diff --git a/src/Laravel/JsonApi/State/JsonApiProvider.php b/src/Laravel/JsonApi/State/JsonApiProvider.php new file mode 100644 index 00000000000..78d605f94a5 --- /dev/null +++ b/src/Laravel/JsonApi/State/JsonApiProvider.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\JsonApi\State; + +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; + +/** + * This is a copy of ApiPlatform\JsonApi\State\JsonApiProvider without the support of sort,filter and fields as these should be implemented using QueryParameters and specific Filters. + * At some point we want to merge both classes but for now we don't have the SortFilter inside Symfony. + * + * @internal + */ +final class JsonApiProvider implements ProviderInterface +{ + public function __construct(private readonly ProviderInterface $decorated) + { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $request = $context['request'] ?? null; + + if (!$request || 'jsonapi' !== $request->getRequestFormat()) { + return $this->decorated->provide($operation, $uriVariables, $context); + } + + $filters = $request->attributes->get('_api_filters', []); + $queryParameters = $request->query->all(); + + $pageParameter = $queryParameters['page'] ?? null; + if ( + \is_array($pageParameter) + ) { + $filters = array_merge($pageParameter, $filters); + } + + if (isset($pageParameter['offset'])) { + $filters['page'] = $pageParameter['offset']; + unset($filters['offset']); + } + + $includeParameter = $queryParameters['include'] ?? null; + + if ($includeParameter) { + $request->attributes->set('_api_included', explode(',', $includeParameter)); + } + + if ($filters) { + $request->attributes->set('_api_filters', $filters); + } + + return $this->decorated->provide($operation, $uriVariables, $context); + } +} diff --git a/src/ParameterValidator/LICENSE b/src/Laravel/LICENSE similarity index 96% rename from src/ParameterValidator/LICENSE rename to src/Laravel/LICENSE index 1ca98eeb824..b228190b358 100644 --- a/src/ParameterValidator/LICENSE +++ b/src/Laravel/LICENSE @@ -1,6 +1,6 @@ The MIT license -Copyright (c) 2015-present Kévin Dunglas +Copyright (c) 2024-present Kévin Dunglas Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Laravel/Metadata/CachePropertyMetadataFactory.php b/src/Laravel/Metadata/CachePropertyMetadataFactory.php new file mode 100644 index 00000000000..f3132a55d30 --- /dev/null +++ b/src/Laravel/Metadata/CachePropertyMetadataFactory.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Metadata; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use Illuminate\Support\Facades\Cache; + +final readonly class CachePropertyMetadataFactory implements PropertyMetadataFactoryInterface +{ + public function __construct( + private PropertyMetadataFactoryInterface $decorated, + private string $cacheStore, + ) { + } + + public function create(string $resourceClass, string $property, array $options = []): ApiProperty + { + $key = hash('xxh3', serialize(['resource_class' => $resourceClass, 'property' => $property] + $options)); + + return Cache::store($this->cacheStore)->rememberForever($key, function () use ($resourceClass, $property, $options) { + return $this->decorated->create($resourceClass, $property, $options); + }); + } +} diff --git a/src/Laravel/Metadata/CachePropertyNameCollectionMetadataFactory.php b/src/Laravel/Metadata/CachePropertyNameCollectionMetadataFactory.php new file mode 100644 index 00000000000..6269dd2080f --- /dev/null +++ b/src/Laravel/Metadata/CachePropertyNameCollectionMetadataFactory.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Metadata; + +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use Illuminate\Support\Facades\Cache; + +final readonly class CachePropertyNameCollectionMetadataFactory implements PropertyNameCollectionFactoryInterface +{ + public function __construct( + private PropertyNameCollectionFactoryInterface $decorated, + private string $cacheStore, + ) { + } + + public function create(string $resourceClass, array $options = []): PropertyNameCollection + { + $key = hash('xxh3', serialize(['resource_class' => $resourceClass] + $options)); + + return Cache::store($this->cacheStore)->rememberForever($key, function () use ($resourceClass, $options) { + return $this->decorated->create($resourceClass, $options); + }); + } +} diff --git a/src/Laravel/Metadata/CacheResourceCollectionMetadataFactory.php b/src/Laravel/Metadata/CacheResourceCollectionMetadataFactory.php new file mode 100644 index 00000000000..83836d9aae4 --- /dev/null +++ b/src/Laravel/Metadata/CacheResourceCollectionMetadataFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Metadata; + +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Illuminate\Support\Facades\Cache; + +final readonly class CacheResourceCollectionMetadataFactory implements ResourceMetadataCollectionFactoryInterface +{ + public function __construct( + private ResourceMetadataCollectionFactoryInterface $decorated, + private string $cacheStore, + ) { + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + return Cache::store($this->cacheStore)->rememberForever($resourceClass, function () use ($resourceClass) { + return $this->decorated->create($resourceClass); + }); + } +} diff --git a/src/Laravel/Metadata/ParameterValidationResourceMetadataCollectionFactory.php b/src/Laravel/Metadata/ParameterValidationResourceMetadataCollectionFactory.php new file mode 100644 index 00000000000..e3741201582 --- /dev/null +++ b/src/Laravel/Metadata/ParameterValidationResourceMetadataCollectionFactory.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Metadata; + +use ApiPlatform\Metadata\Parameter; +use ApiPlatform\Metadata\Parameters; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use Illuminate\Validation\Rule; +use Psr\Container\ContainerInterface; + +final class ParameterValidationResourceMetadataCollectionFactory implements ResourceMetadataCollectionFactoryInterface +{ + public function __construct( + private readonly ?ResourceMetadataCollectionFactoryInterface $decorated = null, + private readonly ?ContainerInterface $filterLocator = null, + ) { + } + + public function create(string $resourceClass): ResourceMetadataCollection + { + $resourceMetadataCollection = $this->decorated?->create($resourceClass) ?? new ResourceMetadataCollection($resourceClass); + + foreach ($resourceMetadataCollection as $i => $resource) { + $operations = $resource->getOperations(); + + foreach ($operations as $operationName => $operation) { + $parameters = $operation->getParameters() ?? new Parameters(); + foreach ($parameters as $key => $parameter) { + $parameters->add($key, $this->addSchemaValidation($parameter)); + } + + if (\count($parameters) > 0) { + $operations->add($operationName, $operation->withParameters($parameters)); + } + } + + $resourceMetadataCollection[$i] = $resource->withOperations($operations->sort()); + + if (!$graphQlOperations = $resource->getGraphQlOperations()) { + continue; + } + + foreach ($graphQlOperations as $operationName => $operation) { + $parameters = $operation->getParameters() ?? new Parameters(); + foreach ($operation->getParameters() ?? [] as $key => $parameter) { + $parameters->add($key, $this->addSchemaValidation($parameter)); + } + + if (\count($parameters) > 0) { + $graphQlOperations[$operationName] = $operation->withParameters($parameters); + } + } + + $resourceMetadataCollection[$i] = $resource->withGraphQlOperations($graphQlOperations); + } + + return $resourceMetadataCollection; + } + + private function addSchemaValidation(Parameter $parameter): Parameter + { + $schema = $parameter->getSchema(); + $required = $parameter->getRequired(); + $openApi = $parameter->getOpenApi(); + + // When it's an array of openapi parameters take the first one as it's probably just a variant of the query parameter, + // only getAllowEmptyValue is used here anyways + if (\is_array($openApi)) { + $openApi = $openApi[0]; + } + $assertions = []; + $allowEmptyValue = $openApi?->getAllowEmptyValue(); + if ($required || (false === $required && false === $allowEmptyValue)) { + $assertions[] = 'required'; + } + + if (true === $allowEmptyValue) { + $assertions[] = 'nullable'; + } + + if (isset($schema['exclusiveMinimum'])) { + $assertions[] = 'gt:'.$schema['exclusiveMinimum']; + } + + if (isset($schema['exclusiveMaximum'])) { + $assertions[] = 'lt:'.$schema['exclusiveMaximum']; + } + + if (isset($schema['minimum'])) { + $assertions[] = 'gte:'.$schema['minimum']; + } + + if (isset($schema['maximum'])) { + $assertions[] = 'lte:'.$schema['maximum']; + } + + if (isset($schema['pattern'])) { + $assertions[] = 'regex:'.$schema['pattern']; + } + + $minLength = isset($schema['minLength']); + $maxLength = isset($schema['maxLength']); + + if ($minLength && $maxLength) { + $assertions[] = \sprintf('between:%s,%s', $schema['minLength'], $schema['maxLength']); + } elseif ($minLength) { + $assertions[] = 'min:'.$schema['minLength']; + } elseif ($maxLength) { + $assertions[] = 'max:'.$schema['maxLength']; + } + + $minItems = isset($schema['minItems']); + $maxItems = isset($schema['maxItems']); + + if ($minItems && $maxItems) { + $assertions[] = \sprintf('between:%s,%s', $schema['minItems'], $schema['maxItems']); + } elseif ($minItems) { + $assertions[] = 'min:'.$schema['minItems']; + } elseif ($maxItems) { + $assertions[] = 'max:'.$schema['maxItems']; + } + + if (isset($schema['multipleOf'])) { + $assertions[] = 'multiple_of:'.$schema['multipleOf']; + } + + if (isset($schema['enum'])) { + $assertions[] = Rule::in($schema['enum']); + } + + if (isset($schema['type']) && 'array' === $schema['type']) { + $assertions[] = 'array'; + } + + if (isset($schema['type']) && 'boolean' === $schema['type']) { + $assertions[] = 'boolean'; + } + + if (!$assertions) { + return $parameter; + } + + if (1 === \count($assertions)) { + return $parameter->withConstraints($assertions[0]); + } + + return $parameter->withConstraints($assertions); + } +} diff --git a/src/Laravel/README.md b/src/Laravel/README.md new file mode 100644 index 00000000000..dd8b2d53c6f --- /dev/null +++ b/src/Laravel/README.md @@ -0,0 +1,12 @@ +# API Platform for Laravel + +Integration of [Laravel](https://laravel.com) and the Illuminate components with the [API Platform](https://api-platform.com) framework. + +[Documentation](https://api-platform.com/docs/laravel/) + +> [!CAUTION] +> +> This is a read-only sub split of `api-platform/core`, please +> [report issues](https://github.com/api-platform/core/issues) and +> [send Pull Requests](https://github.com/api-platform/core/pulls) +> in the [core API Platform repository](https://github.com/api-platform/core). diff --git a/src/Laravel/Routing/IriConverter.php b/src/Laravel/Routing/IriConverter.php new file mode 100644 index 00000000000..460db550e9a --- /dev/null +++ b/src/Laravel/Routing/IriConverter.php @@ -0,0 +1,192 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Routing; + +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Exception\ItemNotFoundException; +use ApiPlatform\Metadata\Exception\OperationNotFoundException; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\IdentifiersExtractorInterface; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Operation\Factory\OperationMetadataFactoryInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Metadata\Util\ClassInfoTrait; +use ApiPlatform\Metadata\Util\ResourceClassInfoTrait; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Database\Eloquent\Relations\Relation; +// use Illuminate\Routing\Router; +use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface; +use Symfony\Component\Routing\RouterInterface; + +class IriConverter implements IriConverterInterface +{ + use ClassInfoTrait; + use ResourceClassInfoTrait; + // use UriVariablesResolverTrait; + + /** + * @var array + */ + private array $localOperationCache = []; + + /** + * @var array + */ + private array $localIdentifiersExtractorOperationCache = []; + + // , UriVariablesConverterInterface $uriVariablesConverter = null TODO + /** + * @param ProviderInterface $provider + */ + public function __construct(private readonly ProviderInterface $provider, private readonly OperationMetadataFactoryInterface $operationMetadataFactory, private readonly RouterInterface $router, private readonly IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly ?IriConverterInterface $decorated = null) + { + $this->resourceClassResolver = $resourceClassResolver; + } + + /** + * @param array $context + */ + public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object + { + $parameters = $this->router->match($iri); + if (!isset($parameters['_api_resource_class'], $parameters['_api_operation_name'], $parameters['uri_variables'])) { + throw new InvalidArgumentException(\sprintf('No resource associated to "%s".', $iri)); + } + + $operation = $this->resourceMetadataCollectionFactory->create($parameters['_api_resource_class'])->getOperation($parameters['_api_operation_name']); + + if ($operation instanceof CollectionOperationInterface) { + throw new InvalidArgumentException(\sprintf('The iri "%s" references a collection not an item.', $iri)); + } + + if (!$operation instanceof HttpOperation) { + throw new RuntimeException(\sprintf('The iri "%s" does not reference an HTTP operation.', $iri)); + } + + if ($item = $this->provider->provide($operation, $parameters['uri_variables'], $context)) { + return $item; // @phpstan-ignore-line + } + + throw new ItemNotFoundException(\sprintf('Item not found for "%s".', $iri)); + } + + /** + * @param array $context + */ + public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): ?string + { + $resourceClass = $context['force_resource_class'] ?? (\is_string($resource) ? $resource : $this->getObjectClass($resource)); + if ($resource instanceof Relation) { + $resourceClass = $this->getObjectClass($resource->getRelated()); + } + + if (isset($context['item_uri_template'])) { + $operation = $this->operationMetadataFactory->create($context['item_uri_template']); + } + + $localOperationCacheKey = ($operation?->getName() ?? '').$resourceClass.(\is_string($resource) ? '_c' : '_i'); + if ($operation && isset($this->localOperationCache[$localOperationCacheKey])) { + return $this->generateRoute($resource, $referenceType, $this->localOperationCache[$localOperationCacheKey], $context, $this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] ?? null); + } + + if (!$this->resourceClassResolver->isResourceClass($resourceClass)) { + return $this->generateSkolemIri($resource, $referenceType, $operation, $context, $resourceClass); + } + + // This is only for when a class (that is not a resource) extends another one that is a resource, we should remove this behavior + if (!\is_string($resource) && !isset($context['force_resource_class'])) { + $resourceClass = $this->getResourceClass($resource, true); + } + + if (!$operation) { + $operation = (new Get())->withClass($resourceClass); + } + + if ($operation instanceof HttpOperation && 301 === $operation->getStatus()) { + $operation = ($operation instanceof CollectionOperationInterface ? new GetCollection() : new Get())->withClass($operation->getClass()); + unset($context['uri_variables']); + } + + $identifiersExtractorOperation = $operation; + // In symfony the operation name is the route name, try to find one if none provided + if ( + !$operation->getName() + || ($operation instanceof HttpOperation && 'POST' === $operation->getMethod()) + ) { + $forceCollection = $operation instanceof CollectionOperationInterface; + try { + $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, $forceCollection, true); + $identifiersExtractorOperation = $operation; + } catch (OperationNotFoundException) { + } + } + + if (!$operation->getName() || ($operation instanceof HttpOperation && $operation->getUriTemplate() && str_starts_with($operation->getUriTemplate(), SkolemIriConverter::SKOLEM_URI_TEMPLATE))) { + return $this->generateSkolemIri($resource, $referenceType, $operation, $context, $resourceClass); + } + + $this->localOperationCache[$localOperationCacheKey] = $operation; + $this->localIdentifiersExtractorOperationCache[$localOperationCacheKey] = $identifiersExtractorOperation; + + return $this->generateRoute($resource, $referenceType, $operation, $context, $identifiersExtractorOperation); + } + + /** + * @param array $context + */ + private function generateRoute(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = [], ?Operation $identifiersExtractorOperation = null): string + { + $identifiers = $context['uri_variables'] ?? []; + + if (\is_object($resource)) { + try { + $identifiers = $this->identifiersExtractor->getIdentifiersFromItem($resource, $identifiersExtractorOperation, $context); + } catch (RuntimeException $e) { + // We can try using context uri variables if any + if (!$identifiers) { + throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e); + } + } + } + + try { + $routeName = $operation instanceof HttpOperation ? ($operation->getRouteName() ?? $operation->getName()) : $operation->getName(); + + return $this->router->generate($routeName, $identifiers, $operation->getUrlGenerationStrategy() ?? $referenceType); + } catch (RoutingExceptionInterface $e) { + throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $operation->getClass()), $e->getCode(), $e); + } + } + + /** + * @param object|class-string $resource + * @param array $context + */ + private function generateSkolemIri(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = [], ?string $resourceClass = null): string + { + if (!$this->decorated) { + throw new InvalidArgumentException(\sprintf('Unable to generate an IRI for the item of type "%s"', $resourceClass)); + } + + // Use a skolem iri, the route is defined in genid.xml + return $this->decorated->getIriFromResource($resource, $referenceType, $operation, $context); + } +} diff --git a/src/Laravel/Routing/Router.php b/src/Laravel/Routing/Router.php new file mode 100644 index 00000000000..e57e59bd0ba --- /dev/null +++ b/src/Laravel/Routing/Router.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Routing; + +use ApiPlatform\Metadata\UrlGeneratorInterface; +use Illuminate\Http\Request as LaravelRequest; +use Illuminate\Routing\Router as BaseRouter; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Generator\UrlGenerator; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\RouterInterface; + +/** + * Laravel router decorator. + * + * @author Kévin Dunglas + */ +final class Router implements RouterInterface, UrlGeneratorInterface +{ + public const CONST_MAP = [ + UrlGeneratorInterface::ABS_URL => RouterInterface::ABSOLUTE_URL, + UrlGeneratorInterface::ABS_PATH => RouterInterface::ABSOLUTE_PATH, + UrlGeneratorInterface::REL_PATH => RouterInterface::RELATIVE_PATH, + UrlGeneratorInterface::NET_PATH => RouterInterface::NETWORK_PATH, + ]; + + private RequestContext $context; + + public function __construct(private readonly BaseRouter $router, private readonly int $urlGenerationStrategy = UrlGeneratorInterface::ABS_PATH) + { + } + + /** + * {@inheritdoc} + */ + public function setContext(RequestContext $context): void + { + $this->context = $context; + } + + /** + * {@inheritdoc} + */ + public function getContext(): RequestContext + { + return $this->context; + } + + /** + * {@inheritdoc} + */ + public function getRouteCollection(): RouteCollection + { + /** @var \Illuminate\Routing\RouteCollection $routes */ + $routes = $this->router->getRoutes(); + + return $routes->toSymfonyRouteCollection(); + } + + /** + * {@inheritdoc} + * + * @return array + * + * @phpstan-return array + * + * @psalm-return array{_api_resource_class?: class-string|string, _api_operation_name?: string, uri_variables?: array, ...} + */ + public function match(string $pathInfo): array + { + $request = LaravelRequest::create($pathInfo, Request::METHOD_GET); + $route = $this->router->getRoutes()->match($request); + + return $route->defaults + ['uri_variables' => array_diff_key($route->parameters, $route->defaults)]; + } + + /** + * {@inheritdoc} + * + * @param array $parameters + */ + public function generate(string $name, array $parameters = [], ?int $referenceType = null): string + { + $routes = $this->getRouteCollection(); + $generator = new UrlGenerator($routes, $this->getContext()); + if (isset($parameters['_format']) && !str_starts_with($parameters['_format'], '.')) { + $parameters['_format'] = '.'.$parameters['_format']; + } + + return $generator->generate($name, $parameters, self::CONST_MAP[$referenceType ?? $this->urlGenerationStrategy]); + } +} diff --git a/src/Laravel/Routing/SkolemIriConverter.php b/src/Laravel/Routing/SkolemIriConverter.php new file mode 100644 index 00000000000..fcb67798536 --- /dev/null +++ b/src/Laravel/Routing/SkolemIriConverter.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Routing; + +use ApiPlatform\Metadata\Exception\ItemNotFoundException; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\UrlGeneratorInterface; + +/** + * {@inheritdoc} + * + * @author Antoine Bluchet + */ +final class SkolemIriConverter implements IriConverterInterface +{ + public const SKOLEM_URI_TEMPLATE = '/.well-known/genid/{id}'; + + /** + * @var \SplObjectStorage + */ + private \SplObjectStorage $objectHashMap; + + /** + * @var array + */ + private array $classHashMap = []; + + public function __construct(private readonly Router $router) + { + $this->objectHashMap = new \SplObjectStorage(); + } + + /** + * {@inheritdoc} + */ + public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object + { + throw new ItemNotFoundException(\sprintf('Item not found for "%s".', $iri)); + } + + /** + * {@inheritdoc} + */ + public function getIriFromResource(object|string $resource, int $referenceType = UrlGeneratorInterface::ABS_PATH, ?Operation $operation = null, array $context = []): string + { + $referenceType = $operation ? ($operation->getUrlGenerationStrategy() ?? $referenceType) : $referenceType; + if (($isObject = \is_object($resource)) && $this->objectHashMap->contains($resource)) { + return $this->router->generate('api_genid', ['id' => $this->objectHashMap[$resource]], $referenceType); + } + + if (\is_string($resource) && isset($this->classHashMap[$resource])) { + return $this->router->generate('api_genid', ['id' => $this->classHashMap[$resource]], $referenceType); + } + + $id = bin2hex(random_bytes(10)); + + if ($isObject) { + $this->objectHashMap[$resource] = $id; + } else { + $this->classHashMap[$resource] = $id; + } + + return $this->router->generate('api_genid', ['id' => $id], $referenceType); + } +} diff --git a/src/Laravel/Security/ResourceAccessChecker.php b/src/Laravel/Security/ResourceAccessChecker.php new file mode 100644 index 00000000000..5633ea1c808 --- /dev/null +++ b/src/Laravel/Security/ResourceAccessChecker.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Security; + +use ApiPlatform\Laravel\Eloquent\Paginator; +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; +use Illuminate\Support\Facades\Gate; + +class ResourceAccessChecker implements ResourceAccessCheckerInterface +{ + public function isGranted(string $resourceClass, string $expression, array $extraVariables = []): bool + { + return Gate::allows( + $expression, + $extraVariables['object'] instanceof Paginator ? + $resourceClass : + $extraVariables['object'] + ); + } +} diff --git a/src/Laravel/ServiceLocator.php b/src/Laravel/ServiceLocator.php new file mode 100644 index 00000000000..c774f8369d0 --- /dev/null +++ b/src/Laravel/ServiceLocator.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel; + +use Psr\Container\ContainerInterface; + +// TODO: template T ServiceLocator +final class ServiceLocator implements ContainerInterface +{ + private array $services = []; + + /** + * @param array $services + */ + public function __construct(array $services = []) + { + foreach ($services as $key => $service) { + $this->services[\is_string($key) ? $key : $service::class] = $service; + } + } + + public function get(string $id): mixed + { + return $this->services[$id] ?? null; + } + + public function has(string $id): bool + { + return isset($this->services[$id]); + } +} diff --git a/src/Laravel/State/AccessCheckerProvider.php b/src/Laravel/State/AccessCheckerProvider.php new file mode 100644 index 00000000000..21b432bf5dc --- /dev/null +++ b/src/Laravel/State/AccessCheckerProvider.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\ResourceAccessCheckerInterface; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Auth\Access\AuthorizationException; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; + +/** + * Allows access based on the ApiPlatform\Metadata\ResourceAccessCheckerInterface. + * This implementation covers GraphQl and HTTP. + * + * @see ResourceAccessCheckerInterface + * + * @implements ProviderInterface + */ +final class AccessCheckerProvider implements ProviderInterface +{ + /** + * @param ProviderInterface $decorated + */ + public function __construct(private readonly ProviderInterface $decorated, private readonly ResourceAccessCheckerInterface $resourceAccessChecker) + { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $policy = $operation->getPolicy(); + $message = $operation->getSecurityMessage(); + + $body = $this->decorated->provide($operation, $uriVariables, $context); + if (null === $policy) { + return $body; + } + + $request = $context['request'] ?? null; + + $resourceAccessCheckerContext = [ + 'object' => $body, + 'request' => $request, + 'operation' => $operation, + ]; + + if (!$this->resourceAccessChecker->isGranted($operation->getClass(), $policy, $resourceAccessCheckerContext)) { + throw $operation instanceof HttpOperation ? new AuthorizationException($message ?? 'Access Denied.') : new AccessDeniedHttpException($message ?? 'Access Denied.'); + } + + return $body; + } +} diff --git a/src/Laravel/State/ParameterValidatorProvider.php b/src/Laravel/State/ParameterValidatorProvider.php new file mode 100644 index 00000000000..d3f4fab3d01 --- /dev/null +++ b/src/Laravel/State/ParameterValidatorProvider.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ParameterNotFound; +use ApiPlatform\State\ProviderInterface; +use ApiPlatform\State\Util\ParameterParserTrait; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; +use Symfony\Component\HttpFoundation\Request; + +/** + * Validates parameters using the Symfony validator. + * + * @implements ProviderInterface + * + * @experimental + */ +final class ParameterValidatorProvider implements ProviderInterface +{ + use ParameterParserTrait; + use ValidationErrorTrait; + + /** + * @param ProviderInterface $decorated + */ + public function __construct( + private readonly ?ProviderInterface $decorated = null, + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + if (!($request = $context['request'] ?? null) instanceof Request) { + return $this->decorated->provide($operation, $uriVariables, $context); + } + + $operation = $request->attributes->get('_api_operation') ?? $operation; + if (!($operation->getQueryParameterValidationEnabled() ?? true)) { + return $this->decorated->provide($operation, $uriVariables, $context); + } + + $allConstraints = []; + foreach ($operation->getParameters() ?? [] as $parameter) { + if (!$constraints = $parameter->getConstraints()) { + continue; + } + + $key = $parameter->getKey(); + if (null === $key) { + throw new RuntimeException('A parameter must have a defined key.'); + } + + $value = $parameter->getValue(); + if ($value instanceof ParameterNotFound) { + $value = null; + } + + // Basically renames our key from order[:property] to order.* to assign the rule properly (see https://laravel.com/docs/11.x/validation#rule-in) + if (str_contains($key, '[:property]')) { + $k = str_replace('[:property]', '', $key); + $allConstraints[$k.'.*'] = $constraints; + continue; + } + + $allConstraints[$key] = $constraints; + } + + $validator = Validator::make($request->query->all(), $allConstraints); + if ($validator->fails()) { + throw $this->getValidationError($validator, new ValidationException($validator)); + } + + return $this->decorated->provide($operation, $uriVariables, $context); + } +} diff --git a/src/Laravel/State/SwaggerUiProcessor.php b/src/Laravel/State/SwaggerUiProcessor.php new file mode 100644 index 00000000000..6a90a23fdc9 --- /dev/null +++ b/src/Laravel/State/SwaggerUiProcessor.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\OpenApi\OpenApi; +use ApiPlatform\OpenApi\Options; +use ApiPlatform\OpenApi\Serializer\NormalizeOperationNameTrait; +use ApiPlatform\State\ProcessorInterface; +use Illuminate\Http\Response; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * @internal + * + * @implements ProcessorInterface + */ +final class SwaggerUiProcessor implements ProcessorInterface +{ + use NormalizeOperationNameTrait; + + /** + * @param array $formats + */ + public function __construct( + private readonly UrlGeneratorInterface $urlGenerator, + private readonly NormalizerInterface $normalizer, + private readonly Options $openApiOptions, + private readonly array $formats = [], + private readonly ?string $oauthClientId = null, + private readonly ?string $oauthClientSecret = null, + private readonly bool $oauthPkce = false, + ) { + } + + /** + * @param OpenApi $openApi + */ + public function process(mixed $openApi, Operation $operation, array $uriVariables = [], array $context = []): Response + { + $request = $context['request'] ?? null; + + $swaggerContext = [ + 'formats' => $this->formats, + 'title' => $openApi->getInfo()->getTitle(), + 'description' => $openApi->getInfo()->getDescription(), + 'originalRoute' => $request->attributes->get('_api_original_route', $request->attributes->get('_route')), + 'originalRouteParams' => $request->attributes->get('_api_original_route_params', $request->attributes->get('_route_params', [])), + ]; + + $swaggerData = [ + 'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']), + 'spec' => $this->normalizer->normalize($openApi, 'json', []), + 'oauth' => [ + 'enabled' => $this->openApiOptions->getOAuthEnabled(), + 'type' => $this->openApiOptions->getOAuthType(), + 'flow' => $this->openApiOptions->getOAuthFlow(), + 'tokenUrl' => $this->openApiOptions->getOAuthTokenUrl(), + 'authorizationUrl' => $this->openApiOptions->getOAuthAuthorizationUrl(), + 'redirectUrl' => $request->getSchemeAndHttpHost().'/vendor/api-platform/swagger-ui/oauth2-redirect.html', + 'scopes' => $this->openApiOptions->getOAuthScopes(), + 'clientId' => $this->oauthClientId, + 'clientSecret' => $this->oauthClientSecret, + 'pkce' => $this->oauthPkce, + ], + ]; + + $status = 200; + $requestedOperation = $request?->attributes->get('_api_requested_operation') ?? null; + if ($request->isMethodSafe() && $requestedOperation && $requestedOperation->getName()) { + // TODO: what if the parameter is named something else then `id`? + $swaggerData['id'] = ($request->attributes->get('_api_original_uri_variables') ?? [])['id'] ?? null; + $swaggerData['queryParameters'] = $request->query->all(); + + $swaggerData['shortName'] = $requestedOperation->getShortName(); + $swaggerData['operationId'] = $this->normalizeOperationName($requestedOperation->getName()); + + [$swaggerData['path'], $swaggerData['method']] = $this->getPathAndMethod($swaggerData); + $status = $requestedOperation->getStatus() ?? $status; + } + + return new Response(view('api-platform::swagger-ui', $swaggerContext + ['swagger_data' => $swaggerData]), 200); + } + + /** + * @param array $swaggerData + * + * @return array{0: string, 1: string} + */ + private function getPathAndMethod(array $swaggerData): array + { + foreach ($swaggerData['spec']['paths'] as $path => $operations) { + foreach ($operations as $method => $operation) { + if (($operation['operationId'] ?? null) === $swaggerData['operationId']) { + return [$path, $method]; + } + } + } + + throw new RuntimeException(\sprintf('The operation "%s" cannot be found in the Swagger specification.', $swaggerData['operationId'])); + } +} diff --git a/src/Laravel/State/SwaggerUiProvider.php b/src/Laravel/State/SwaggerUiProvider.php new file mode 100644 index 00000000000..a465e5c1748 --- /dev/null +++ b/src/Laravel/State/SwaggerUiProvider.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Documentation\Documentation; +use ApiPlatform\Metadata\Error; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; +use ApiPlatform\OpenApi\OpenApi; +use ApiPlatform\State\ProviderInterface; + +/** + * When an HTML request is sent we provide a swagger ui documentation. + * + * @implements ProviderInterface + * + * @internal + */ +final class SwaggerUiProvider implements ProviderInterface +{ + /** + * @param ProviderInterface $decorated + */ + public function __construct( + private readonly ProviderInterface $decorated, + private readonly OpenApiFactoryInterface $openApiFactory, + private readonly bool $swaggerUiEnabled = true, + ) { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + // We went through the DocumentationAction + if (OpenApi::class === $operation->getClass()) { + return $this->decorated->provide($operation, $uriVariables, $context); + } + + if ( + !($operation instanceof HttpOperation) + || !($request = $context['request'] ?? null) + || 'html' !== $request->getRequestFormat() + || !$this->swaggerUiEnabled + || true === ($operation->getExtraProperties()['_api_disable_swagger_provider'] ?? false) + ) { + return $this->decorated->provide($operation, $uriVariables, $context); + } + + if (!$request->attributes->has('_api_requested_operation')) { + $request->attributes->set('_api_requested_operation', $operation); + } + + // We need to call our operation provider just in case it fails + // when it fails we'll get an Error, and we'll fix the status accordingly + // @see features/main/content_negotiation.feature:119 + // When requesting DocumentationAction or EntrypointAction with Accept: text/html we render SwaggerUi + if (!$operation instanceof Error && Documentation::class !== $operation->getClass()) { + $this->decorated->provide($operation, $uriVariables, $context); + } + + $swaggerUiOperation = new Get( + class: OpenApi::class, + processor: 'api_platform.swagger_ui.processor', + validate: false, + read: false, + write: true, // force write so that our processor gets called + status: $operation->getStatus() + ); + + // save our operation + $request->attributes->set('_api_operation', $swaggerUiOperation); + + $data = $this->openApiFactory->__invoke([ + 'base_url' => $request->getBaseUrl() ?: '/', + 'filter_tags' => $request->query->all('filter_tags'), + ]); + $request->attributes->set('data', $data); + + return $data; + } +} diff --git a/src/Laravel/State/ValidateProvider.php b/src/Laravel/State/ValidateProvider.php new file mode 100644 index 00000000000..3fc959f48a9 --- /dev/null +++ b/src/Laravel/State/ValidateProvider.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Metadata\Error; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\State\ProviderInterface; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * @implements ProviderInterface + */ +final class ValidateProvider implements ProviderInterface +{ + use ValidationErrorTrait; + + /** + * @param ProviderInterface $inner + */ + public function __construct( + private readonly ProviderInterface $inner, + private readonly Application $app, + private readonly ?NormalizerInterface $normalizer = null, + ?NameConverterInterface $nameConverter = null, + ) { + if (!$normalizer) { + trigger_deprecation('api-platform/laravel', '4.2', 'Not using the normalizer in %s is deprecated.', self::class); + } + + $this->nameConverter = $nameConverter; + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $body = $this->inner->provide($operation, $uriVariables, $context); + + if ($operation instanceof Error) { + return $body; + } + + $rules = $operation->getRules(); + if (\is_callable($rules)) { + $rules = $rules(); + } + + if (\is_string($rules) && is_a($rules, FormRequest::class, true)) { + try { + // this also throws an AuthorizationException + $this->app->make($rules); + } catch (ValidationException $e) { // @phpstan-ignore-line make->($rules) may throw this + if (!$operation->canValidate()) { + return $body; + } + + throw $this->getValidationError($e->validator, $e); + } + + return $body; + } + + if (!$operation->canValidate()) { + return $body; + } + + if (!\is_array($rules)) { + return $body; + } + + $validationBody = $this->getBodyForValidation($body); + + $validator = Validator::make($validationBody, $rules); + if ($validator->fails()) { + throw $this->getValidationError($validator, new ValidationException($validator)); + } + + return $body; + } + + /** + * @return array + */ + private function getBodyForValidation(mixed $body): array + { + if (!$body) { + return []; + } + + if ($body instanceof Model) { + return $body->toArray(); + } + + if ($this->normalizer) { + if (!\is_array($v = $this->normalizer->normalize($body))) { + throw new RuntimeException('An array is expected.'); + } + + return $v; + } + + // hopefully this path never gets used, its there for BC-layer only + // TODO: remove in 5.0 + if ($s = json_encode($body)) { + return json_decode($s, true); + } + + throw new RuntimeException('Could not transform the denormalized body in an array for validation'); + } +} diff --git a/src/Laravel/State/ValidationErrorTrait.php b/src/Laravel/State/ValidationErrorTrait.php new file mode 100644 index 00000000000..b6a358dcb04 --- /dev/null +++ b/src/Laravel/State/ValidationErrorTrait.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\State; + +use ApiPlatform\Laravel\ApiResource\ValidationError; +use Illuminate\Contracts\Validation\Validator; +use Illuminate\Validation\ValidationException; +use Symfony\Component\Serializer\NameConverter\NameConverterInterface; + +trait ValidationErrorTrait +{ + private ?NameConverterInterface $nameConverter = null; + + private function getValidationError(Validator $validator, ValidationException $e): ValidationError + { + $errors = $validator->errors(); + $violations = []; + $id = hash('xxh3', implode(',', $errors->keys())); + foreach ($errors->messages() as $prop => $message) { + $violations[] = ['propertyPath' => $this->nameConverter ? $this->nameConverter->normalize($prop) : $prop, 'message' => implode(\PHP_EOL, $message)]; + } + + return new ValidationError($e->getMessage(), $id, $e, $violations); + } +} diff --git a/src/Laravel/Test/ApiTestAssertionsTrait.php b/src/Laravel/Test/ApiTestAssertionsTrait.php new file mode 100644 index 00000000000..9752fe134a1 --- /dev/null +++ b/src/Laravel/Test/ApiTestAssertionsTrait.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Test; + +use ApiPlatform\Laravel\Test\Constraint\ArraySubset; +use ApiPlatform\Metadata\IriConverterInterface; +use PHPUnit\Framework\ExpectationFailedException; + +trait ApiTestAssertionsTrait +{ + /** + * Asserts that an array has a specified subset. + * + * Imported from dms/phpunit-arraysubset, because the original constraint has been deprecated. + * + * @copyright Sebastian Bergmann + * @copyright Rafael Dohms + * + * @see https://github.com/sebastianbergmann/phpunit/issues/3494 + * + * @param array $subset + * @param array $array + * + * @throws ExpectationFailedException + * @throws \Exception + */ + public static function assertArraySubset(iterable $subset, iterable $array, bool $checkForObjectIdentity = false, string $message = ''): void + { + $constraint = new ArraySubset($subset, $checkForObjectIdentity); + + static::assertThat($array, $constraint, $message); + } + + /** + * Asserts that the retrieved JSON contains the specified subset. + * + * This method delegates to static::assertArraySubset(). + * + * @param array $subset + * @param array $json + */ + public static function assertJsonContains(array|string $subset, array $json, bool $checkForObjectIdentity = true, string $message = ''): void + { + if (\is_string($subset)) { + $subset = json_decode($subset, true, 512, \JSON_THROW_ON_ERROR); + } + if (!\is_array($subset)) { + throw new \InvalidArgumentException('$subset must be array or string (JSON array or JSON object)'); + } + + static::assertArraySubset($subset, $json, $checkForObjectIdentity, $message); + } + + /** + * Generates the IRI of a resource item. + */ + protected function getIriFromResource(object $resource): ?string + { + $iriConverter = $this->app->make(IriConverterInterface::class); + + return $iriConverter->getIriFromResource($resource); + } +} diff --git a/src/Laravel/Test/Constraint/ArraySubset.php b/src/Laravel/Test/Constraint/ArraySubset.php new file mode 100644 index 00000000000..9727721c70b --- /dev/null +++ b/src/Laravel/Test/Constraint/ArraySubset.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Test\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; + +/** + * Is used for phpunit >= 9. + * + * @internal + */ +final class ArraySubset extends Constraint +{ + use ArraySubsetTrait; + + /** + * {@inheritdoc} + */ + public function evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool + { + return $this->_evaluate($other, $description, $returnResult); + } +} diff --git a/src/Laravel/Test/Constraint/ArraySubsetTrait.php b/src/Laravel/Test/Constraint/ArraySubsetTrait.php new file mode 100644 index 00000000000..692b6a2ada4 --- /dev/null +++ b/src/Laravel/Test/Constraint/ArraySubsetTrait.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Test\Constraint; + +use SebastianBergmann\Comparator\ComparisonFailure; +use SebastianBergmann\Exporter\Exporter; + +/** + * Constraint that asserts that the array it is evaluated for has a specified subset. + * + * Uses array_replace_recursive() to check if a key value subset is part of the + * subject array. + * + * Imported from dms/phpunit-arraysubset-asserts, because the original constraint has been deprecated. + * + * @copyright Sebastian Bergmann + * @copyright Rafael Dohms + * + * @see https://github.com/sebastianbergmann/phpunit/issues/3494 + */ +trait ArraySubsetTrait +{ + /** + * @param array $subset + */ + public function __construct(private iterable $subset, private readonly bool $strict = false) + { + } + + private function _evaluate(mixed $other, string $description = '', bool $returnResult = false): ?bool + { + // type cast $other & $this->subset as an array to allow + // support in standard array functions. + $other = $this->toArray($other); + $this->subset = $this->toArray($this->subset); + $patched = array_replace_recursive($other, $this->subset); + if ($this->strict) { + $result = $other === $patched; + } else { + $result = $other == $patched; + } + if ($returnResult) { + return $result; + } + if ($result) { + return null; + } + + $f = new ComparisonFailure( + $patched, + $other, + var_export($patched, true), + var_export($other, true) + ); + $this->fail($other, $description, $f); + } + + /** + * {@inheritdoc} + */ + public function toString(): string + { + return 'has the subset '.(new Exporter())->export($this->subset); + } + + /** + * {@inheritdoc} + */ + protected function failureDescription(mixed $other): string + { + return 'an array '.$this->toString(); + } + + /** + * @param array $other + * + * @return array + */ + private function toArray(iterable $other): array + { + if (\is_array($other)) { + return $other; + } + if ($other instanceof \ArrayObject) { + return $other->getArrayCopy(); + } + + return iterator_to_array($other); + } +} diff --git a/src/Laravel/Tests/ApiTest.php b/src/Laravel/Tests/ApiTest.php new file mode 100644 index 00000000000..b99e6bdd8de --- /dev/null +++ b/src/Laravel/Tests/ApiTest.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class ApiTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.routes.domain', 'http://test.com/'); + $config->set('app.debug', true); + $config->set('api-platform.formats', ['jsonld' => ['application/ld+json']]); + $config->set('api-platform.docs_formats', ['jsonld' => ['application/ld+json']]); + }); + } + + public function testDomainCanBeSet(): void + { + $response = $this->get('http://foobar.com/api/', ['accept' => ['application/ld+json']]); + $response->assertNotFound(); + + $response = $this->get('http://test.com/api/', ['accept' => ['application/ld+json']]); + $response->assertSuccessful(); + } + + public function testPrefixedOperations(): void + { + $response = $this->post('http://test.com/billing/calculate', [], ['content-type' => ['application/ld+json']]); + $response->assertSuccessful(); + + $response = $this->post('http://test.com/shipping/calculate', [], ['content-type' => ['application/ld+json']]); + $response->assertSuccessful(); + } +} diff --git a/src/Laravel/Tests/AuthTest.php b/src/Laravel/Tests/AuthTest.php new file mode 100644 index 00000000000..4b7d3bc8c9b --- /dev/null +++ b/src/Laravel/Tests/AuthTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\Database\Factories\UserFactory; + +class AuthTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.graphql.enabled', true); + $config->set('app.key', 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF'); + }); + } + + protected function afterRefreshingDatabase(): void + { + UserFactory::new()->create(); + } + + public function testGetCollection(): void + { + $response = $this->get('/api/vaults', ['accept' => ['application/ld+json']]); + $this->assertArraySubset(['detail' => 'Unauthenticated.'], $response->json()); + $response->assertHeader('content-type', 'application/problem+json; charset=utf-8'); + $response->assertStatus(401); + } + + public function testAuthenticated(): void + { + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->get('/api/vaults', ['accept' => ['application/ld+json'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(200); + } + + public function testAuthenticatedPolicy(): void + { + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->postJson('/api/vaults', [], ['accept' => ['application/ld+json'], 'content-type' => ['application/ld+json'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(403); + } + + public function testAuthenticatedDeleteWithPolicy(): void + { + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->delete('/api/vaults/1', [], ['accept' => ['application/ld+json'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(403); + } +} diff --git a/src/Laravel/Tests/AutoconfigureTest.php b/src/Laravel/Tests/AutoconfigureTest.php new file mode 100644 index 00000000000..ea0246fe479 --- /dev/null +++ b/src/Laravel/Tests/AutoconfigureTest.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class AutoconfigureTest extends TestCase +{ + use ApiTestAssertionsTrait; + use WithWorkbench; + + public function testServiceProvider(): void + { + $response = $this->get('/api/custom_service_provider', headers: ['accept' => ['application/ld+json']]); + $this->assertEquals($response->json()['test'], 'ok'); + $response->assertSuccessful(); + } + + public function testServiceProviderWithDependency(): void + { + $response = $this->get('/api/custom_service_provider_with_dependency', headers: ['accept' => ['application/ld+json']]); + $this->assertEquals($response->json()['test'], 'test'); + $response->assertSuccessful(); + } +} diff --git a/src/Laravel/Tests/Console/Maker/MakeFilterCommandTest.php b/src/Laravel/Tests/Console/Maker/MakeFilterCommandTest.php new file mode 100644 index 00000000000..ce71d1a3fd2 --- /dev/null +++ b/src/Laravel/Tests/Console/Maker/MakeFilterCommandTest.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Console\Maker; + +use ApiPlatform\Laravel\Tests\Console\Maker\Utils\AppServiceFileGenerator; +use ApiPlatform\Laravel\Tests\Console\Maker\Utils\PathResolver; +use Illuminate\Console\Command; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +class MakeFilterCommandTest extends TestCase +{ + use WithWorkbench; + + /** @var string */ + private const MAKE_FILTER_COMMAND = 'make:filter'; + /** @var string */ + private const FILTER_CLASS_NAME = 'Choose a class name for your filter (e.g. AwesomeFilter)'; + + private Filesystem $filesystem; + private PathResolver $pathResolver; + private AppServiceFileGenerator $appServiceFileGenerator; + + /** + * @throws FileNotFoundException + */ + protected function setup(): void + { + parent::setUp(); + + $this->filesystem = new Filesystem(); + $this->pathResolver = new PathResolver(); + $this->appServiceFileGenerator = new AppServiceFileGenerator($this->filesystem); + + $this->appServiceFileGenerator->regenerateProviderFile(); + } + + /** + * @throws FileNotFoundException + */ + public function testMakeStateFilterCommand(): void + { + $filterName = 'MyFilter'; + $filePath = $this->pathResolver->generateFilterFilename($filterName); + $appServiceFilterPath = $this->pathResolver->getServiceProviderFilePath(); + + $this->artisan(self::MAKE_FILTER_COMMAND) + ->expectsQuestion(self::FILTER_CLASS_NAME, $filterName) + ->expectsOutputToContain('Success!') + ->expectsOutputToContain("created: $filePath") + ->expectsOutputToContain('Next: Open your new Eloquent Filter class and start customizing it.') + ->assertExitCode(Command::SUCCESS); + + $this->assertFileExists($filePath); + + $appServiceFilterContent = $this->filesystem->get($appServiceFilterPath); + $this->assertStringContainsString('use App\\Filter\\MyFilter;', $appServiceFilterContent); + $this->assertStringContainsString('use ApiPlatform\Laravel\Eloquent\Filter\FilterInterface;', $appServiceFilterContent); + $this->assertStringContainsString('$this->app->tag(MyFilter::class, FilterInterface::class);', $appServiceFilterContent); + + $this->filesystem->delete($filePath); + } + + public function testWhenStateFilterClassAlreadyExists(): void + { + $filterName = 'ExistingFilter'; + $existingFile = $this->pathResolver->generateFilterFilename($filterName); + $this->filesystem->put($existingFile, 'artisan(self::MAKE_FILTER_COMMAND) + ->expectsQuestion(self::FILTER_CLASS_NAME, $filterName) + ->expectsOutput($expectedError) + ->assertExitCode(Command::FAILURE); + + $this->filesystem->delete($existingFile); + } + + #[DataProvider('nullProvider')] + public function testMakeStateFilterCommandWithoutGivenClassName(?string $value): void + { + $this->artisan(self::MAKE_FILTER_COMMAND) + ->expectsQuestion(self::FILTER_CLASS_NAME, $value) + ->assertExitCode(Command::FAILURE); + } + + public static function nullProvider(): \Generator + { + yield 'null value used' => ['value' => null]; + yield 'empty string used' => ['value' => '']; + } + + /** + * @throws FileNotFoundException + */ + protected function tearDown(): void + { + parent::tearDown(); + + $this->appServiceFileGenerator->regenerateProviderFile(); + } +} diff --git a/src/Laravel/Tests/Console/Maker/MakeStateProcessorCommandTest.php b/src/Laravel/Tests/Console/Maker/MakeStateProcessorCommandTest.php new file mode 100644 index 00000000000..b21b55d1d2d --- /dev/null +++ b/src/Laravel/Tests/Console/Maker/MakeStateProcessorCommandTest.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Console\Maker; + +use ApiPlatform\Laravel\Tests\Console\Maker\Utils\AppServiceFileGenerator; +use ApiPlatform\Laravel\Tests\Console\Maker\Utils\PathResolver; +use Illuminate\Console\Command; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +class MakeStateProcessorCommandTest extends TestCase +{ + use WithWorkbench; + + /** @var string */ + private const STATE_PROCESSOR_COMMAND = 'make:state-processor'; + /** @var string */ + private const CHOSEN_CLASS_NAME = 'Choose a class name for your state processor (e.g. AwesomeStateProcessor)'; + + private Filesystem $filesystem; + private PathResolver $pathResolver; + private AppServiceFileGenerator $appServiceFileGenerator; + + /** + * @throws FileNotFoundException + */ + protected function setup(): void + { + parent::setUp(); + + $this->filesystem = new Filesystem(); + $this->pathResolver = new PathResolver(); + $this->appServiceFileGenerator = new AppServiceFileGenerator($this->filesystem); + + $this->appServiceFileGenerator->regenerateProviderFile(); + } + + /** + * @throws FileNotFoundException + */ + public function testMakeStateProviderCommand(): void + { + $processorName = 'MyStateProcessor'; + $filePath = $this->pathResolver->generateStateFilename($processorName); + $appServiceProviderPath = $this->pathResolver->getServiceProviderFilePath(); + + $this->artisan(self::STATE_PROCESSOR_COMMAND) + ->expectsQuestion(self::CHOSEN_CLASS_NAME, $processorName) + ->expectsOutputToContain('Success!') + ->expectsOutputToContain("created: $filePath") + ->expectsOutputToContain('Next: Open your new State Processor class and start customizing it.') + ->assertExitCode(Command::SUCCESS); + + $this->assertFileExists($filePath); + + $appServiceProviderContent = $this->filesystem->get($appServiceProviderPath); + $this->assertStringContainsString('use ApiPlatform\State\ProcessorInterface;', $appServiceProviderContent); + $this->assertStringContainsString("use App\State\\$processorName;", $appServiceProviderContent); + $this->assertStringContainsString('$this->app->tag(MyStateProcessor::class, ProcessorInterface::class);', $appServiceProviderContent); + + $this->filesystem->delete($filePath); + } + + public function testWhenStateProviderClassAlreadyExists(): void + { + $processorName = 'ExistingProcessor'; + $existingFile = $this->pathResolver->generateStateFilename($processorName); + $this->filesystem->put($existingFile, 'artisan(self::STATE_PROCESSOR_COMMAND) + ->expectsQuestion(self::CHOSEN_CLASS_NAME, $processorName) + ->expectsOutput($expectedError) + ->assertExitCode(Command::FAILURE); + + $this->filesystem->delete($existingFile); + } + + #[DataProvider('nullProvider')] + public function testMakeStateFilterCommandWithoutGivenClassName(?string $value): void + { + $this->artisan(self::STATE_PROCESSOR_COMMAND) + ->expectsQuestion(self::CHOSEN_CLASS_NAME, $value) + ->assertExitCode(Command::FAILURE); + } + + public static function nullProvider(): \Generator + { + yield 'null value used' => ['value' => null]; + yield 'empty string used' => ['value' => '']; + } + + /** + * @throws FileNotFoundException + */ + protected function tearDown(): void + { + parent::tearDown(); + + $this->appServiceFileGenerator->regenerateProviderFile(); + } +} diff --git a/src/Laravel/Tests/Console/Maker/MakeStateProviderCommandTest.php b/src/Laravel/Tests/Console/Maker/MakeStateProviderCommandTest.php new file mode 100644 index 00000000000..5334bdb1a91 --- /dev/null +++ b/src/Laravel/Tests/Console/Maker/MakeStateProviderCommandTest.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Console\Maker; + +use ApiPlatform\Laravel\Tests\Console\Maker\Utils\AppServiceFileGenerator; +use ApiPlatform\Laravel\Tests\Console\Maker\Utils\PathResolver; +use Illuminate\Console\Command; +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +class MakeStateProviderCommandTest extends TestCase +{ + use WithWorkbench; + + /** @var string */ + private const MAKE_STATE_PROVIDER_COMMAND = 'make:state-provider'; + /** @var string */ + private const STATE_PROVIDER_CLASS_NAME = 'Choose a class name for your state provider (e.g. AwesomeStateProvider)'; + + private Filesystem $filesystem; + private PathResolver $pathResolver; + private AppServiceFileGenerator $appServiceFileGenerator; + + /** + * @throws FileNotFoundException + */ + protected function setup(): void + { + parent::setUp(); + + $this->filesystem = new Filesystem(); + $this->pathResolver = new PathResolver(); + $this->appServiceFileGenerator = new AppServiceFileGenerator($this->filesystem); + + $this->appServiceFileGenerator->regenerateProviderFile(); + } + + /** + * @throws FileNotFoundException + */ + public function testMakeStateProviderCommand(): void + { + $providerName = 'MyStateProvider'; + $filePath = $this->pathResolver->generateStateFilename($providerName); + $appServiceProviderPath = $this->pathResolver->getServiceProviderFilePath(); + + $this->artisan(self::MAKE_STATE_PROVIDER_COMMAND) + ->expectsQuestion(self::STATE_PROVIDER_CLASS_NAME, $providerName) + ->expectsOutputToContain('Success!') + ->expectsOutputToContain("created: $filePath") + ->expectsOutputToContain('Next: Open your new State Provider class and start customizing it.') + ->assertExitCode(Command::SUCCESS); + + $this->assertFileExists($filePath); + + $appServiceProviderContent = $this->filesystem->get($appServiceProviderPath); + $this->assertStringContainsString('use ApiPlatform\State\ProviderInterface;', $appServiceProviderContent); + $this->assertStringContainsString("use App\State\\$providerName;", $appServiceProviderContent); + $this->assertStringContainsString('$this->app->tag(MyStateProvider::class, ProviderInterface::class);', $appServiceProviderContent); + + $this->filesystem->delete($filePath); + } + + public function testWhenStateProviderClassAlreadyExists(): void + { + $providerName = 'ExistingProvider'; + $existingFile = $this->pathResolver->generateStateFilename($providerName); + $this->filesystem->put($existingFile, 'artisan(self::MAKE_STATE_PROVIDER_COMMAND) + ->expectsQuestion(self::STATE_PROVIDER_CLASS_NAME, $providerName) + ->expectsOutput($expectedError) + ->assertExitCode(Command::FAILURE); + + $this->filesystem->delete($existingFile); + } + + #[DataProvider('nullProvider')] + public function testMakeStateFilterCommandWithoutGivenClassName(?string $value): void + { + $this->artisan(self::MAKE_STATE_PROVIDER_COMMAND) + ->expectsQuestion(self::STATE_PROVIDER_CLASS_NAME, $value) + ->assertExitCode(Command::FAILURE); + } + + public static function nullProvider(): \Generator + { + yield 'null value used' => ['value' => null]; + yield 'empty string used' => ['value' => '']; + } + + /** + * @throws FileNotFoundException + */ + protected function tearDown(): void + { + parent::tearDown(); + + $this->appServiceFileGenerator->regenerateProviderFile(); + } +} diff --git a/src/Laravel/Tests/Console/Maker/Resources/skeleton/AppServiceProvider.php.tpl b/src/Laravel/Tests/Console/Maker/Resources/skeleton/AppServiceProvider.php.tpl new file mode 100644 index 00000000000..ff02bb3a99d --- /dev/null +++ b/src/Laravel/Tests/Console/Maker/Resources/skeleton/AppServiceProvider.php.tpl @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Providers; + +use Illuminate\Support\ServiceProvider; + +class AppServiceProvider extends ServiceProvider +{ + public function boot(): void + { + } + + public function register(): void + { + } +} diff --git a/src/Laravel/Tests/Console/Maker/Utils/AppServiceFileGenerator.php b/src/Laravel/Tests/Console/Maker/Utils/AppServiceFileGenerator.php new file mode 100644 index 00000000000..a85272c0223 --- /dev/null +++ b/src/Laravel/Tests/Console/Maker/Utils/AppServiceFileGenerator.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Console\Maker\Utils; + +use Illuminate\Contracts\Filesystem\FileNotFoundException; +use Illuminate\Filesystem\Filesystem; + +final readonly class AppServiceFileGenerator +{ + public function __construct(private Filesystem $filesystem) + { + } + + /** + * @throws FileNotFoundException + */ + public function regenerateProviderFile(): void + { + $templatePath = \dirname(__DIR__).'/Resources/skeleton/AppServiceProvider.php.tpl'; + $targetPath = base_path('app/Providers/AppServiceProvider.php'); + + $this->regenerateFileFromTemplate($templatePath, $targetPath); + } + + /** + * @throws FileNotFoundException + */ + private function regenerateFileFromTemplate(string $templatePath, string $targetPath): void + { + $content = $this->filesystem->get($templatePath); + + $this->filesystem->put($targetPath, $content); + } +} diff --git a/src/Laravel/Tests/Console/Maker/Utils/PathResolver.php b/src/Laravel/Tests/Console/Maker/Utils/PathResolver.php new file mode 100644 index 00000000000..0ddaeb29ddf --- /dev/null +++ b/src/Laravel/Tests/Console/Maker/Utils/PathResolver.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Console\Maker\Utils; + +final readonly class PathResolver +{ + public function getServiceProviderFilePath(): string + { + return base_path('app/Providers/AppServiceProvider.php'); + } + + public function generateFilterFilename(string $stateFilename): string + { + return \sprintf('%s/app/Filter/%s.php', base_path(), $stateFilename); + } + + public function generateStateFilename(string $stateFilename): string + { + return \sprintf('%s/app/State/%s.php', base_path(), $stateFilename); + } +} diff --git a/src/Laravel/Tests/DocsTest.php b/src/Laravel/Tests/DocsTest.php new file mode 100644 index 00000000000..9d155f041d3 --- /dev/null +++ b/src/Laravel/Tests/DocsTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class DocsTest extends TestCase +{ + use ApiTestAssertionsTrait; + use WithWorkbench; + + public function testOpenApi(): void + { + $res = $this->get('/api/docs.jsonopenapi'); + $this->assertArrayHasKey('openapi', $res->json()); + $this->assertSame('application/vnd.openapi+json; charset=utf-8', $res->headers->get('content-type')); + } + + public function testOpenApiAccept(): void + { + $res = $this->get('/api/docs', headers: ['accept' => 'application/vnd.openapi+json']); + $this->assertArrayHasKey('openapi', $res->json()); + $this->assertSame('application/vnd.openapi+json; charset=utf-8', $res->headers->get('content-type')); + } + + public function testJsonLd(): void + { + $res = $this->get('/api/docs.jsonld'); + $this->assertArrayHasKey('@context', $res->json()); + $this->assertSame('application/ld+json; charset=utf-8', $res->headers->get('content-type')); + } + + public function testJsonLdAccept(): void + { + $res = $this->get('/api/docs', headers: ['accept' => 'application/ld+json']); + $this->assertArrayHasKey('@context', $res->json()); + $this->assertSame('application/ld+json; charset=utf-8', $res->headers->get('content-type')); + } +} diff --git a/src/Laravel/Tests/Eloquent/Filter/BooleanFilterTest.php b/src/Laravel/Tests/Eloquent/Filter/BooleanFilterTest.php new file mode 100644 index 00000000000..b3f837c85b9 --- /dev/null +++ b/src/Laravel/Tests/Eloquent/Filter/BooleanFilterTest.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Eloquent\Filter; + +use ApiPlatform\Laravel\Eloquent\Filter\BooleanFilter; +use ApiPlatform\Metadata\QueryParameter; +use Illuminate\Database\Eloquent\Builder; +use PHPUnit\Framework\TestCase; + +final class BooleanFilterTest extends TestCase +{ + public function testOperator(): void + { + $f = new BooleanFilter(); + $builder = $this->createStub(Builder::class); + $this->assertEquals($builder, $f->apply($builder, ['is_active' => 'true'], new QueryParameter(key: 'isActive', property: 'is_active'))); + } +} diff --git a/src/Laravel/Tests/Eloquent/Filter/DateFilterTest.php b/src/Laravel/Tests/Eloquent/Filter/DateFilterTest.php new file mode 100644 index 00000000000..2cac345483a --- /dev/null +++ b/src/Laravel/Tests/Eloquent/Filter/DateFilterTest.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Eloquent\Filter; + +use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; +use ApiPlatform\Metadata\QueryParameter; +use Illuminate\Database\Eloquent\Builder; +use PHPUnit\Framework\TestCase; + +class DateFilterTest extends TestCase +{ + public function testOperator(): void + { + $f = new DateFilter(); + $builder = $this->createStub(Builder::class); + $this->assertEquals($builder, $f->apply($builder, ['neq' => '2020-02-02'], new QueryParameter(key: 'date', property: 'date'))); + } +} diff --git a/src/Laravel/Tests/Eloquent/Filter/OrderFilterTest.php b/src/Laravel/Tests/Eloquent/Filter/OrderFilterTest.php new file mode 100644 index 00000000000..ce4f1b6f333 --- /dev/null +++ b/src/Laravel/Tests/Eloquent/Filter/OrderFilterTest.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\DB; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\Database\Factories\ActiveBookFactory; + +class OrderFilterTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + public function testQueryParameterWithCamelCaseProperty(): void + { + ActiveBookFactory::new(['is_active' => true])->count(2)->create(); + ActiveBookFactory::new(['is_active' => false])->count(3)->create(); + + DB::enableQueryLog(); + $response = $this->get('/api/active_books?sort[isActive]=asc', ['Accept' => ['application/ld+json']]); + $response->assertStatus(200); + $this->assertEquals(\DB::getQueryLog()[1]['query'], 'select * from "active_books" order by "isActive" asc limit 30 offset 0'); + DB::flushQueryLog(); + $response = $this->get('/api/active_books?sort[isActive]=desc', ['Accept' => ['application/ld+json']]); + $response->assertStatus(200); + $this->assertEquals(DB::getQueryLog()[1]['query'], 'select * from "active_books" order by "isActive" desc limit 30 offset 0'); + } +} diff --git a/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php b/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php new file mode 100644 index 00000000000..c76babf0703 --- /dev/null +++ b/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Eloquent\Metadata; + +use ApiPlatform\Laravel\Eloquent\Metadata\ModelMetadata; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Book; + +/** + * @author Tobias Oitzinger + */ +final class ModelMetadataTest extends TestCase +{ + use RefreshDatabase; + use WithWorkbench; + + public function testHiddenAttributesAreCorrectlyIdentified(): void + { + $model = new class extends Model { + protected $hidden = ['secret']; + + /** + * @return HasMany + */ + public function secret(): HasMany // @phpstan-ignore-line + { + return $this->hasMany(Book::class); // @phpstan-ignore-line + } + }; + + $metadata = new ModelMetadata(); + $this->assertCount(0, $metadata->getRelations($model)); + } + + public function testVisibleAttributesAreCorrectlyIdentified(): void + { + $model = new class extends Model { + protected $visible = ['secret']; + + /** + * @return HasMany + */ + public function secret(): HasMany // @phpstan-ignore-line + { + return $this->hasMany(Book::class); // @phpstan-ignore-line + } + }; + + $metadata = new ModelMetadata(); + $this->assertCount(1, $metadata->getRelations($model)); + } + + public function testAllAttributesVisibleByDefault(): void + { + $model = new class extends Model { + /** + * @return HasMany + */ + public function secret(): HasMany // @phpstan-ignore-line + { + return $this->hasMany(Book::class); // @phpstan-ignore-line + } + }; + + $metadata = new ModelMetadata(); + $this->assertCount(1, $metadata->getRelations($model)); + } +} diff --git a/src/Laravel/Tests/EloquentTest.php b/src/Laravel/Tests/EloquentTest.php new file mode 100644 index 00000000000..6ff86dc9812 --- /dev/null +++ b/src/Laravel/Tests/EloquentTest.php @@ -0,0 +1,635 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use ApiPlatform\Laravel\workbench\app\Enums\BookStatus; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Str; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Http\Requests\StoreSlotRequest; +use Workbench\App\Models\PostWithMorphMany; +use Workbench\Database\Factories\AreaFactory; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; +use Workbench\Database\Factories\CommentMorphFactory; +use Workbench\Database\Factories\GrandSonFactory; +use Workbench\Database\Factories\PostWithMorphManyFactory; +use Workbench\Database\Factories\TimeSlotFactory; +use Workbench\Database\Factories\WithAccessorFactory; + +class EloquentTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + public function testBackedEnumsNormalization(): void + { + BookFactory::new([ + 'status' => BookStatus::DRAFT, + ])->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + + $this->assertArrayHasKey('status', $book); + $this->assertSame('DRAFT', $book['status']); + } + + public function testSearchFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + + $response = $this->get('/api/books?isbn='.$book['isbn'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0], $book); + } + + public function testValidateSearchFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books?isbn=a', ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['detail'], 'The isbn field must be at least 2 characters.'); + } + + public function testSearchFilterRelation(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books?author=1', ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['author'], '/api/authors/1'); + } + + public function testPropertyFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + + $response = $this->get(\sprintf('%s.jsonld?properties[]=author', $book['@id'])); + $book = $response->json(); + + $this->assertArrayHasKey('@id', $book); + $this->assertArrayHasKey('author', $book); + $this->assertArrayNotHasKey('name', $book); + } + + public function testPartialSearchFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + + if (!isset($book['name'])) { + throw new \UnexpectedValueException(); + } + + $end = strpos($book['name'], ' ') ?: 3; + $name = substr($book['name'], 0, $end); + + $response = $this->get('/api/books?name='.$name, ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0], $book); + } + + public function testDateFilterEqual(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + $updated = $this->patchJson( + $book['@id'], + ['publicationDate' => '2024-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationDate[eq]='.$updated['publicationDate'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $book['@id']); + } + + public function testDateFilterIncludeNull(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + $updated = $this->patchJson( + $book['@id'], + ['publicationDate' => null], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationWithNulls[gt]=9999-12-31', ['Accept' => ['application/ld+json']]); + $this->assertGreaterThan(0, $response->json()['totalItems']); + } + + public function testDateFilterExcludeNull(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + $updated = $this->patchJson( + $book['@id'], + ['publicationDate' => null], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationDate[gt]=9999-12-31', ['Accept' => ['application/ld+json']]); + $this->assertSame(0, $response->json()['totalItems']); + } + + public function testDateFilterGreaterThan(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $updated = $this->patchJson( + $bookBefore['@id'], + ['publicationDate' => '9998-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $this->patchJson( + $bookAfter['@id'], + ['publicationDate' => '9999-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationDate[gt]='.$updated['publicationDate'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $bookAfter['@id']); + $this->assertSame($response->json()['totalItems'], 1); + } + + public function testDateFilterLowerThanEqual(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $this->patchJson( + $bookBefore['@id'], + ['publicationDate' => '0001-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $this->patchJson( + $bookAfter['@id'], + ['publicationDate' => '0002-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationDate[lte]=0002-02-18', ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $bookBefore['@id']); + $this->assertSame($response->json()['member'][1]['@id'], $bookAfter['@id']); + $this->assertSame($response->json()['totalItems'], 2); + } + + public function testDateFilterBetween(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $book = $response->json()['member'][0]; + $updated = $this->patchJson( + $book['@id'], + ['publicationDate' => '0001-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $book2 = $response->json()['member'][1]; + $this->patchJson( + $book2['@id'], + ['publicationDate' => '0002-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $book3 = $response->json()['member'][2]; + $updated3 = $this->patchJson( + $book3['@id'], + ['publicationDate' => '0003-02-18 00:00:00'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('/api/books?publicationDate[gte]='.substr($updated['publicationDate'], 0, 10).'&publicationDate[lt]='.substr($updated3['publicationDate'], 0, 10), ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $book['@id']); + $this->assertSame($response->json()['member'][1]['@id'], $book2['@id']); + $this->assertSame($response->json()['totalItems'], 2); + } + + public function testSearchFilterWithPropertyPlaceholder(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/authors', ['Accept' => ['application/ld+json']])->json(); + $author = $response['member'][0]; + + $test = $this->get('/api/authors?name='.explode(' ', $author['name'])[0], ['Accept' => ['application/ld+json']])->json(); + $this->assertSame($test['member'][0]['id'], $author['id']); + + $test = $this->get('/api/authors?id='.$author['id'], ['Accept' => ['application/ld+json']])->json(); + $this->assertSame($test['member'][0]['id'], $author['id']); + } + + public function testOrderFilterWithPropertyPlaceholder(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $res = $this->get('/api/authors?order[id]=desc', ['Accept' => ['application/ld+json']])->json(); + $this->assertSame($res['member'][0]['id'], 10); + } + + public function testOrFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']])->json()['member']; + $book = $response[0]; + $book2 = $response[1]; + + $res = $this->get(\sprintf('/api/books?name2[]=%s&name2[]=%s', $book['name'], $book2['name']), ['Accept' => ['application/ld+json']])->json(); + $this->assertSame($res['totalItems'], 2); + } + + public function testRangeLowerThanFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $this->patchJson( + $bookBefore['@id'], + ['isbn' => '12'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $updated = $this->patchJson( + $bookAfter['@id'], + ['isbn' => '15'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('api/books?isbn_range[lt]='.$updated['isbn'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $bookBefore['@id']); + $this->assertSame($response->json()['totalItems'], 1); + } + + public function testRangeLowerThanEqualFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $this->patchJson( + $bookBefore['@id'], + ['isbn' => '12'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $updated = $this->patchJson( + $bookAfter['@id'], + ['isbn' => '15'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('api/books?isbn_range[lte]='.$updated['isbn'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $bookBefore['@id']); + $this->assertSame($response->json()['member'][1]['@id'], $bookAfter['@id']); + $this->assertSame($response->json()['totalItems'], 2); + } + + public function testRangeGreaterThanFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $updated = $this->patchJson( + $bookBefore['@id'], + ['isbn' => '999999999999998'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $this->patchJson( + $bookAfter['@id'], + ['isbn' => '999999999999999'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $response = $this->get('api/books?isbn_range[gt]='.$updated['isbn'], ['Accept' => ['application/ld+json']]); + $this->assertSame($response->json()['member'][0]['@id'], $bookAfter['@id']); + $this->assertSame($response->json()['totalItems'], 1); + } + + public function testRangeGreaterThanEqualFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['Accept' => ['application/ld+json']]); + $bookBefore = $response->json()['member'][0]; + $updated = $this->patchJson( + $bookBefore['@id'], + ['isbn' => '999999999999998'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + + $bookAfter = $response->json()['member'][1]; + $this->patchJson( + $bookAfter['@id'], + ['isbn' => '999999999999999'], + [ + 'Accept' => ['application/ld+json'], + 'Content-Type' => ['application/merge-patch+json'], + ] + ); + $response = $this->get('api/books?isbn_range[gte]='.$updated['isbn'], ['Accept' => ['application/ld+json']]); + $json = $response->json(); + $this->assertSame($json['member'][0]['@id'], $bookBefore['@id']); + $this->assertSame($json['member'][1]['@id'], $bookAfter['@id']); + $this->assertSame($json['totalItems'], 2); + } + + public function testWrongOrderFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $res = $this->get('/api/authors?order[name]=something', ['Accept' => ['application/ld+json']]); + $this->assertEquals($res->getStatusCode(), 422); + } + + public function testWithAccessor(): void + { + WithAccessorFactory::new()->create(); + $res = $this->get('/api/with_accessors/1', ['Accept' => ['application/ld+json']]); + $this->assertArraySubset(['name' => 'test'], $res->json()); + } + + public function testBooleanFilter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $res = $this->get('/api/books?published=notabool', ['Accept' => ['application/ld+json']]); + $this->assertEquals($res->getStatusCode(), 422); + + $res = $this->get('/api/books?published=0', ['Accept' => ['application/ld+json']]); + $this->assertEquals($res->getStatusCode(), 200); + $this->assertEquals($res->json()['totalItems'], 0); + } + + public function testBelongsTo(): void + { + GrandSonFactory::new()->count(1)->create(); + + $res = $this->get('/api/grand_sons/1/grand_father', ['Accept' => ['application/ld+json']]); + $json = $res->json(); + $this->assertEquals($json['@id'], '/api/grand_sons/1/grand_father'); + $this->assertEquals($json['sons'][0], '/api/grand_sons/1'); + } + + public function testHasMany(): void + { + GrandSonFactory::new()->count(1)->create(); + + $res = $this->get('/api/grand_fathers/1/grand_sons', ['Accept' => ['application/ld+json']]); + $json = $res->json(); + $this->assertEquals($json['@id'], '/api/grand_fathers/1/grand_sons'); + $this->assertEquals($json['totalItems'], 1); + $this->assertEquals($json['member'][0]['@id'], '/api/grand_sons/1'); + } + + public function testRelationIsHandledOnCreateWithNestedData(): void + { + $cartData = [ + 'productSku' => 'SKU_TEST_001', + 'quantity' => 2, + 'priceAtAddition' => '19.99', + 'shoppingCart' => [ + 'userIdentifier' => 'user-'.Str::uuid()->toString(), + 'status' => 'active', + ], + ]; + + $response = $this->postJson('/api/cart_items', $cartData, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + $response->assertStatus(201); + + $response + ->assertJson([ + '@context' => '/api/contexts/CartItem', + '@id' => '/api/cart_items/1', + '@type' => 'CartItem', + 'id' => 1, + 'productSku' => 'SKU_TEST_001', + 'quantity' => 2, + 'priceAtAddition' => 19.99, + 'shoppingCart' => [ + '@id' => '/api/shopping_carts/1', + '@type' => 'ShoppingCart', + 'userIdentifier' => $cartData['shoppingCart']['userIdentifier'], + 'status' => 'active', + ], + ]); + } + + public function testRelationIsHandledOnCreateWithNestedDataToMany(): void + { + $cartData = [ + 'userIdentifier' => 'user-'.Str::uuid()->toString(), + 'status' => 'active', + 'cartItems' => [ + [ + 'productSku' => 'SKU_TEST_001', + 'quantity' => 2, + 'priceAtAddition' => '19.99', + ], + [ + 'productSku' => 'SKU_TEST_002', + 'quantity' => 1, + 'priceAtAddition' => '25.50', + ], + ], + ]; + + $response = $this->postJson('/api/shopping_carts', $cartData, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + $response->assertStatus(201); + $response->assertJson([ + '@context' => '/api/contexts/ShoppingCart', + '@id' => '/api/shopping_carts/1', + '@type' => 'ShoppingCart', + 'id' => 1, + 'userIdentifier' => $cartData['userIdentifier'], + 'status' => 'active', + 'cartItems' => [ + [ + '@id' => '/api/cart_items/1', + '@type' => 'CartItem', + 'productSku' => 'SKU_TEST_001', + 'quantity' => 2, + 'priceAtAddition' => '19.99', + ], + [ + '@id' => '/api/cart_items/2', + '@type' => 'CartItem', + 'productSku' => 'SKU_TEST_002', + 'quantity' => 1, + 'priceAtAddition' => '25.50', + ], + ], + ]); + } + + public function testPostWithEmptyMorphMany(): void + { + $response = $this->postJson('/api/post_with_morph_manies', [ + 'title' => 'My first post', + 'content' => 'This is the content of my first post.', + 'comments' => [['content' => 'hello']], + ], ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + $response->assertStatus(201); + $response->assertJson([ + 'title' => 'My first post', + 'content' => 'This is the content of my first post.', + 'comments' => [['content' => 'hello']], + ]); + } + + public function testPostCommentsCollectionFromMorphMany(): void + { + PostWithMorphManyFactory::new()->create(); + + CommentMorphFactory::new()->count(5)->create([ + 'commentable_id' => 1, + 'commentable_type' => PostWithMorphMany::class, + ]); + + $response = $this->getJson('/api/post_with_morph_manies/1/comments', [ + 'accept' => 'application/ld+json', + ]); + $response->assertStatus(200); + $response->assertJsonCount(5, 'member'); + } + + public function testPostCommentItemFromMorphMany(): void + { + PostWithMorphManyFactory::new()->create(); + + CommentMorphFactory::new()->count(5)->create([ + 'commentable_id' => 1, + 'commentable_type' => PostWithMorphMany::class, + ])->first(); + + $response = $this->getJson('/api/post_with_morph_manies/1/comments/1', [ + 'accept' => 'application/ld+json', + ]); + $response->assertStatus(200); + $response->assertJson([ + '@context' => '/api/contexts/CommentMorph', + '@id' => '/api/post_with_morph_manies/1/comments/1', + '@type' => 'CommentMorph', + 'id' => 1, + ]); + } + + public function testCreateDeliveryRequestWithPickupSlot(): void + { + $pickupTimeSlot = TimeSlotFactory::new()->create(['note' => 'Morning slot']); + + $response = $this->postJson('/api/delivery_requests', [ + 'pickupTimeSlot' => '/api/time_slots/'.$pickupTimeSlot->id, // @phpstan-ignore-line + 'note' => 'This is a test note.', + ], ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + + $response->assertStatus(201); + $response->assertJson([ + '@context' => '/api/contexts/DeliveryRequest', + '@id' => '/api/delivery_requests/1', + '@type' => 'DeliveryRequest', + 'pickupTimeSlot' => [ + '@id' => '/api/time_slots/'.$pickupTimeSlot->id, // @phpstan-ignore-line + '@type' => 'TimeSlot', + 'name' => $pickupTimeSlot->name, // @phpstan-ignore-line + 'note' => $pickupTimeSlot->note, // @phpstan-ignore-line + ], + 'note' => 'This is a test note.', + ]); + } + + public function testIriIsNotDenormalizedBeforeFormRequestValidation(): void + { + $area = AreaFactory::new()->create(); + + $this->postJson( + '/api/slots', + [ + 'name' => 'Morning Slot', + 'area' => '/api/areas/'.$area->id, // @phpstan-ignore-line + ], + ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json'] + )->assertStatus(201); + + $this->assertSame(StoreSlotRequest::$receivedArea->name, $area->name); // @phpstan-ignore-line + } +} diff --git a/src/Laravel/Tests/GraphQlAuthTest.php b/src/Laravel/Tests/GraphQlAuthTest.php new file mode 100644 index 00000000000..2af920dd618 --- /dev/null +++ b/src/Laravel/Tests/GraphQlAuthTest.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Attributes\DefineEnvironment; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; +use Workbench\Database\Factories\UserFactory; +use Workbench\Database\Factories\VaultFactory; + +class GraphQlAuthTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + protected function afterRefreshingDatabase(): void + { + UserFactory::new()->create(); + } + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.routes.middleware', ['auth:sanctum']); + $config->set('app.key', 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF'); + $config->set('api-platform.graphql.enabled', true); + }); + } + + public function testUnauthenticated(): void + { + $response = $this->postJson('/api/graphql', [], []); + $response->assertStatus(401); + } + + public function testAuthenticated(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->get('/api/graphql', ['accept' => ['text/html'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(200); + $response = $this->postJson('/api/graphql', [ + 'query' => '{books { edges { node {id, name, publicationDate, author {id, name }}}}}', + ], [ + 'content-type' => 'application/json', + 'authorization' => 'Bearer '.$token, + ]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('data', $data); + $this->assertArrayNotHasKey('errors', $data); + } + + public function testPolicy(): void + { + VaultFactory::new()->count(10)->create(); + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->postJson('/api/graphql', ['query' => 'mutation { + updateVault(input: {secret: "secret", id: "/api/vaults/1"}) { + vault {id} + } + } +'], ['accept' => ['application/json'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('errors', $data); + $this->assertEquals('Access Denied.', $data['errors'][0]['message']); + } + + /** + * @param Application $app + */ + protected function useProductionMode($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.routes.middleware', ['auth:sanctum']); + $config->set('api-platform.graphql.enabled', true); + $config->set('app.key', 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF'); + $config->set('app.debug', false); + }); + } + + #[DefineEnvironment('useProductionMode')] + public function testProductionError(): void + { + VaultFactory::new()->count(10)->create(); + $response = $this->post('/tokens/create'); + $token = $response->json()['token']; + $response = $this->postJson('/api/graphql', ['query' => 'mutation { + updateVault(input: {secret: "secret", id: "/api/vaults/1"}) { + vault {id} + } + } +'], ['accept' => ['application/json'], 'authorization' => 'Bearer '.$token]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('errors', $data); + $this->assertArrayNotHasKey('trace', $data['errors'][0]); + } +} diff --git a/src/Laravel/Tests/GraphQlTest.php b/src/Laravel/Tests/GraphQlTest.php new file mode 100644 index 00000000000..bd1829c2abc --- /dev/null +++ b/src/Laravel/Tests/GraphQlTest.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use ApiPlatform\Laravel\workbench\app\Enums\BookStatus; +use Illuminate\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; + +class GraphQlTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.graphql.enabled', true); + }); + } + + public function testGetBooks(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->postJson('/api/graphql', ['query' => '{books { edges { node {id, name, publicationDate, author {id, name }}}}}'], ['accept' => ['application/json']]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('data', $data); + $this->assertArrayNotHasKey('errors', $data); + } + + public function testGetBooksWithSimplePagination(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(9)->create(); + $response = $this->postJson('/api/graphql', ['query' => '{ + simplePaginationBooks(page: 1) { + collection { + id + }, + paginationInfo { + itemsPerPage, + currentPage, + lastPage, + totalCount, + hasNextPage + } + } +}'], ['accept' => ['application/json']]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('data', $data); + $this->assertCount(3, $data['data']['simplePaginationBooks']['collection']); + $this->assertEquals(3, $data['data']['simplePaginationBooks']['paginationInfo']['itemsPerPage']); + $this->assertEquals(1, $data['data']['simplePaginationBooks']['paginationInfo']['currentPage']); + $this->assertEquals(3, $data['data']['simplePaginationBooks']['paginationInfo']['lastPage']); + $this->assertEquals(9, $data['data']['simplePaginationBooks']['paginationInfo']['totalCount']); + $this->assertTrue($data['data']['simplePaginationBooks']['paginationInfo']['hasNextPage']); + $this->assertArrayNotHasKey('errors', $data); + } + + public function testGetBooksWithPaginationAndOrder(): void + { + // Create books in reverse alphabetical order to test the 'asc' order + BookFactory::new() + ->count(10) + ->sequence(fn ($sequence) => ['name' => \chr(122 - $sequence->index)]) // ASCII codes starting from 'z' + ->has(AuthorFactory::new()) + ->create(); + + $response = $this->postJson('/api/graphql', [ + 'query' => ' + query getBooks($first: Int!, $order: orderBookcollection_query!) { + books(first: $first, order: $order) { + edges { + node { + id, name, publicationDate, author { id, name } + } + } + } + } + ', + 'variables' => [ + 'first' => 3, + 'order' => ['name' => 'asc'], + ], + ], ['accept' => ['application/json']]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('data', $data); + $this->assertCount(3, $data['data']['books']['edges']); + $this->assertEquals('q', $data['data']['books']['edges'][0]['node']['name']); + $this->assertEquals('r', $data['data']['books']['edges'][1]['node']['name']); + $this->assertEquals('s', $data['data']['books']['edges'][2]['node']['name']); + $this->assertArrayNotHasKey('errors', $data); + } + + public function testCreateBook(): void + { + /** @var \Workbench\App\Models\Author $author */ + $author = AuthorFactory::new()->create(); + $response = $this->postJson('/api/graphql', [ + 'query' => ' + mutation createBook($book: createBookInput!){ + createBook(input: $book){ + book{ + name + isAvailable + } + } + } + ', + 'variables' => [ + 'book' => [ + 'name' => fake()->name(), + 'author' => 'api/authors/'.$author->id, + 'isbn' => fake()->isbn13(), + 'status' => BookStatus::PUBLISHED, + 'isAvailable' => 1 === random_int(0, 1), + ], + ], + ], ['accept' => ['application/json']]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayNotHasKey('errors', $data); + $this->assertArrayHasKey('data', $data); + $this->assertArrayHasKey('createBook', $data['data']); + $this->assertArrayHasKey('book', $data['data']['createBook']); + $this->assertArrayHasKey('isAvailable', $data['data']['createBook']['book']); + $this->assertIsBool($data['data']['createBook']['book']['isAvailable']); + } +} diff --git a/src/Laravel/Tests/HalTest.php b/src/Laravel/Tests/HalTest.php new file mode 100644 index 00000000000..b73f9bcf03c --- /dev/null +++ b/src/Laravel/Tests/HalTest.php @@ -0,0 +1,118 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Book; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; + +class HalTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonhal' => ['application/hal+json']]); + $config->set('api-platform.docs_formats', ['jsonhal' => ['application/hal+json']]); + $config->set('app.debug', true); + }); + } + + public function testGetEntrypoint(): void + { + $response = $this->get('/api/', ['accept' => ['application/hal+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/hal+json; charset=utf-8'); + + $this->assertJsonContains( + [ + '_links' => [ + 'self' => ['href' => '/api'], + 'book' => ['href' => '/api/books'], + 'post' => ['href' => '/api/posts'], + 'sluggable' => ['href' => '/api/sluggables'], + 'vault' => ['href' => '/api/vaults'], + 'author' => ['href' => '/api/authors'], + ], + ], + $response->json() + ); + } + + public function testGetCollection(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['accept' => 'application/hal+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/hal+json; charset=utf-8'); + $this->assertJsonContains( + [ + '_links' => [ + 'first' => ['href' => '/api/books?page=1'], + 'self' => ['href' => '/api/books?page=1'], + 'last' => ['href' => '/api/books?page=2'], + ], + 'totalItems' => 10, + ], + $response->json() + ); + } + + public function testGetBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/hal+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/hal+json; charset=utf-8'); + $this->assertJsonContains( + [ + 'name' => $book->name, // @phpstan-ignore-line + 'isbn' => $book->isbn, // @phpstan-ignore-line + '_links' => [ + 'self' => [ + 'href' => $iri, + ], + 'author' => [ + 'href' => '/api/authors/1', + ], + ], + ], + $response->json() + ); + } + + public function testDeleteBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/hal+json']); + $response->assertStatus(204); + $this->assertNull(Book::find($book->id)); + } +} diff --git a/src/Laravel/Tests/JsonApiTest.php b/src/Laravel/Tests/JsonApiTest.php new file mode 100644 index 00000000000..ea618179f24 --- /dev/null +++ b/src/Laravel/Tests/JsonApiTest.php @@ -0,0 +1,334 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\JsonApi\Filter\SparseFieldset; +use ApiPlatform\Laravel\Eloquent\Filter\JsonApi\SortFilter; +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use ApiPlatform\Metadata\QueryParameter; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\DB; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; +use Workbench\Database\Factories\WithAccessorFactory; + +class JsonApiTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.resources', [app_path('Models'), app_path('ApiResource')]); + $config->set('api-platform.pagination.items_per_page_parameter_name', 'limit'); + $config->set('api-platform.defaults', [ + 'route_prefix' => '/api', + 'parameters' => [ + new QueryParameter(key: 'fields', filter: SparseFieldset::class), + new QueryParameter(key: 'sort', filter: SortFilter::class), + ], + ]); + + $config->set('app.debug', true); + }); + } + + public function testGetEntrypoint(): void + { + $response = $this->get('/api/', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + $this->assertJsonContains( + [ + 'links' => [ + 'self' => 'http://localhost/api', + 'book' => 'http://localhost/api/books', + ], + ], + $response->json() + ); + } + + public function testGetCollection(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + $response->assertJsonFragment([ + 'links' => [ + 'self' => '/api/books?page=1', + 'first' => '/api/books?page=1', + 'last' => '/api/books?page=2', + 'next' => '/api/books?page=2', + ], + 'meta' => ['totalItems' => 10, 'itemsPerPage' => 5, 'currentPage' => 1], + ]); + $response->assertJsonCount(5, 'data'); + } + + public function testGetBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + + $this->assertJsonContains([ + 'data' => [ + 'id' => $iri, + 'type' => 'Book', + 'attributes' => [ + 'name' => $book->name, // @phpstan-ignore-line + ], + ], + ], $response->json()); + } + + public function testCreateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'data' => [ + 'attributes' => [ + 'name' => 'Don Quichotte', + 'isbn' => fake()->isbn13(), + 'publicationDate' => fake()->optional()->date(), + ], + 'relationships' => [ + 'author' => [ + 'data' => [ + 'id' => $this->getIriFromResource($author), + 'type' => 'Author', + ], + ], + ], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + + $response->assertStatus(201); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + $this->assertJsonContains([ + 'data' => [ + 'type' => 'Book', + 'attributes' => [ + 'name' => 'Don Quichotte', + ], + ], + ], $response->json()); + $this->assertMatchesRegularExpression('~^/api/books/~', $response->json('data.id')); + } + + public function testUpdateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->putJson( + $iri, + [ + 'data' => ['attributes' => ['name' => 'updated title']], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + $response->assertStatus(200); + $this->assertJsonContains([ + 'data' => [ + 'id' => $iri, + 'attributes' => [ + 'name' => 'updated title', + ], + ], + ], $response->json()); + } + + public function testPatchBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->patchJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(200); + $this->assertJsonContains([ + 'data' => [ + 'id' => $iri, + 'attributes' => [ + 'name' => 'updated title', + ], + ], + ], $response->json()); + } + + public function testDeleteBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/vnd.api+json']); + $response->assertStatus(204); + $this->assertNull(Book::find($book->id)); + } + + public function testRelationWithGroups(): void + { + WithAccessorFactory::new()->create(); + $response = $this->get('/api/with_accessors/1', ['accept' => 'application/vnd.api+json']); + $content = $response->json(); + $this->assertArrayHasKey('data', $content); + $this->assertArrayHasKey('relationships', $content['data']); + $this->assertArrayHasKey('relation', $content['data']['relationships']); + $this->assertArrayHasKey('data', $content['data']['relationships']['relation']); + } + + public function testValidateJsonApi(): void + { + $response = $this->postJson( + '/api/issue6745/rule_validations', + [ + 'data' => [ + 'type' => 'string', + 'attributes' => ['max' => 3], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + + $response->assertStatus(422); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + $json = $response->json(); + $this->assertJsonContains([ + 'errors' => [ + [ + 'detail' => 'The prop field is required.', + 'title' => 'Validation Error', + 'status' => 422, + 'code' => '58350900e0fc6b8e/prop', + ], + [ + 'detail' => 'The max field must be less than 2.', + 'title' => 'Validation Error', + 'status' => 422, + 'code' => '58350900e0fc6b8e/max', + ], + ], + ], $json); + + $this->assertArrayHasKey('id', $json['errors'][0]); + $this->assertArrayHasKey('links', $json['errors'][0]); + $this->assertArrayHasKey('type', $json['errors'][0]['links']); + + $response = $this->postJson( + '/api/issue6745/rule_validations', + [ + 'data' => [ + 'type' => 'string', + 'attributes' => [ + 'prop' => 1, + 'max' => 1, + ], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + $response->assertStatus(201); + } + + public function testNotFound(): void + { + $response = $this->get('/api/books/notfound', headers: ['accept' => 'application/vnd.api+json']); + $response->assertStatus(404); + $response->assertHeader('content-type', 'application/vnd.api+json; charset=utf-8'); + + $this->assertJsonContains([ + 'links' => ['type' => '/errors/404'], + 'title' => 'An error occurred', + 'status' => 404, + 'detail' => 'Not Found', + ], $response->json()['errors'][0]); + } + + public function testSortParameter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + DB::enableQueryLog(); + $this->get('/api/books?sort=isbn,-name', headers: ['accept' => 'application/vnd.api+json']); + ['query' => $q] = DB::getQueryLog()[1]; + $this->assertStringContainsString('order by "isbn" asc, "name" desc', $q); + } + + public function testPageParameter(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + DB::enableQueryLog(); + $this->get('/api/books?page[limit]=1&page[offset]=2', headers: ['accept' => 'application/vnd.api+json']); + ['query' => $q] = DB::getQueryLog()[1]; + $this->assertStringContainsString('select * from "books" limit 1 offset 1', $q); + } + + public function testSparseFieldset(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $r = $this->get('/api/books?fields[book]=name,isbn&fields[author]=name&include=author', headers: ['accept' => 'application/vnd.api+json']); + $res = $r->json(); + $attributes = $res['data'][0]['attributes']; + $this->assertArrayHasKey('name', $attributes); + $this->assertArrayHasKey('isbn', $attributes); + $this->assertArrayNotHasKey('isAvailable', $attributes); + + $included = $res['included'][0]['attributes']; + $this->assertArrayNotHasKey('createdAt', $included); + $this->assertArrayHasKey('name', $included); + } +} diff --git a/src/Laravel/Tests/JsonLdTest.php b/src/Laravel/Tests/JsonLdTest.php new file mode 100644 index 00000000000..9ee75969652 --- /dev/null +++ b/src/Laravel/Tests/JsonLdTest.php @@ -0,0 +1,381 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; +use Workbench\Database\Factories\CommentFactory; +use Workbench\Database\Factories\PostFactory; +use Workbench\Database\Factories\SluggableFactory; +use Workbench\Database\Factories\WithAccessorFactory; + +class JsonLdTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('app.debug', true); + $config->set('api-platform.resources', [app_path('Models'), app_path('ApiResource')]); + }); + } + + public function testGetCollection(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Book', + '@id' => '/api/books', + '@type' => 'Collection', + 'totalItems' => 10, + ]); + $response->assertJsonCount(5, 'member'); + } + + public function testGetBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $response = $this->get($this->getIriFromResource($book), ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Book', + '@id' => $this->getIriFromResource($book), + '@type' => 'Book', + 'name' => $book->name, // @phpstan-ignore-line + ]); + } + + public function testCreateBook(): void + { + AuthorFactory::new()->create(); + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'name' => 'Don Quichotte', + 'author' => $this->getIriFromResource($author), + 'isbn' => fake()->isbn13(), + 'publicationDate' => fake()->optional()->date(), + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/ld+json', + ] + ); + + $response->assertStatus(201); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Book', + '@type' => 'Book', + 'name' => 'Don Quichotte', + ]); + $this->assertMatchesRegularExpression('~^/api/books/~', $response->json('@id')); + } + + public function testUpdateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->putJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/ld+json', + ] + ); + $response->assertStatus(200); + $response->assertJsonFragment([ + 'name' => 'updated title', + ]); + } + + public function testPatchBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->patchJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(200); + $response->assertJsonFragment([ + '@id' => $iri, + 'name' => 'updated title', + ]); + } + + public function testDeleteBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/ld+json']); + $response->assertStatus(204); + $this->assertNull(Book::find($book->id)); + } + + public function testPatchBookAuthor(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $author = Author::find(2); + $authorIri = $this->getIriFromResource($author); + $response = $this->patchJson( + $iri, + [ + 'author' => $authorIri, + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(200); + $response->assertJsonFragment([ + '@id' => $iri, + 'author' => $authorIri, + ]); + } + + public function testSkolemIris(): void + { + $response = $this->get('/api/outputs', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@type' => 'NotAResource', + 'name' => 'test', + ]); + + $this->assertMatchesRegularExpression('~^/api/.well-known/genid/~', $response->json('@id')); + } + + public function testSubresourceCollection(): void + { + PostFactory::new()->has(CommentFactory::new()->count(10))->count(10)->create(); + $response = $this->get('/api/posts', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + + $response->assertJsonFragment([ + '@context' => '/api/contexts/Post', + '@id' => '/api/posts', + '@type' => 'Collection', + 'totalItems' => 10, + ]); + $response->assertJsonCount(10, 'member'); + $postIri = $response->json('member.0.@id'); + $commentsIri = $response->json('member.0.comments'); + $this->assertMatchesRegularExpression('~^/api/posts/\d+/comments$~', $commentsIri); + $response = $this->get($commentsIri, ['accept' => 'application/ld+json']); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Comment', + '@id' => $commentsIri, + '@type' => 'Collection', + 'totalItems' => 10, + ]); + + $commentIri = $response->json('member.0.@id'); + $response = $this->get($commentIri, ['accept' => 'application/ld+json']); + $response->assertJsonFragment([ + '@id' => $commentIri, + 'post' => $postIri, + ]); + } + + public function testCreateNotValid(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'name' => 'Don Quichotte', + 'author' => $this->getIriFromResource($author), + 'isbn' => 'test@foo', + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/ld+json', + ] + ); + + $response->assertStatus(422); + $response->assertHeader('content-type', 'application/problem+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/ValidationError', + '@type' => 'ValidationError', + 'description' => 'The isbn field must only contain letters and numbers.', + ]); + + $violations = $response->json('violations'); + $this->assertCount(1, $violations); + $this->assertEquals($violations[0], ['propertyPath' => 'isbn', 'message' => 'The isbn field must only contain letters and numbers.']); + } + + public function testCreateNotValidPost(): void + { + $response = $this->postJson( + '/api/posts', + [ + ], + [ + 'accept' => 'application/ld+json', + 'content_type' => 'application/ld+json', + ] + ); + + $response->assertStatus(422); + $response->assertHeader('content-type', 'application/problem+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/ValidationError', + '@type' => 'ValidationError', + 'description' => 'The title field is required.', + ]); + + $violations = $response->json('violations'); + $this->assertCount(1, $violations); + $this->assertEquals($violations[0], ['propertyPath' => 'title', 'message' => 'The title field is required.']); + } + + public function testSluggable(): void + { + SluggableFactory::new()->count(10)->create(); + $response = $this->get('/api/sluggables', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Sluggable', + '@id' => '/api/sluggables', + '@type' => 'Collection', + 'totalItems' => 10, + ]); + $iri = $response->json('member.0.@id'); + $response = $this->get($iri, ['accept' => 'application/ld+json']); + $response->assertStatus(200); + } + + public function testApiDocsRegex(): void + { + $response = $this->get('/api/notexists', ['accept' => 'application/ld+json']); + $response->assertNotFound(); + } + + public function testHidden(): void + { + PostFactory::new()->has(CommentFactory::new()->count(10))->count(10)->create(); + $response = $this->get('/api/posts/1/comments/1', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonMissingPath('internalNote'); + } + + public function testVisible(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $this->assertStringNotContainsString('internalNote', (string) $response->getContent()); + } + + public function testError(): void + { + $response = $this->post('/api/books', ['content-type' => 'application/vnd.api+json']); + $response->assertStatus(415); + $content = $response->json(); + $this->assertArrayHasKey('trace', $content); + } + + public function testErrorNotFound(): void + { + $response = $this->get('/api/books/asd', ['accept' => 'application/ld+json']); + $response->assertStatus(404); + $content = $response->json(); + $this->assertArrayHasKey('status', $content); + $this->assertEquals(404, $content['status']); + } + + public function testRelationWithGroups(): void + { + WithAccessorFactory::new()->create(); + $response = $this->get('/api/with_accessors/1', ['accept' => 'application/ld+json']); + $content = $response->json(); + $this->assertArrayHasKey('relation', $content); + $this->assertArrayHasKey('name', $content['relation']); + } + + /** + * @see https://github.com/api-platform/core/issues/6779 + */ + public function testSimilarRoutesWithFormat(): void + { + $response = $this->get('/api/staff_position_histories?page=1', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $this->assertSame('/api/staff_position_histories', $response->json()['@id']); + } + + public function testResourceWithOptionModel(): void + { + $response = $this->get('/api/resource_with_models?page=1', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + $response->assertHeader('content-type', 'application/ld+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/ResourceWithModel', + '@id' => '/api/resource_with_models', + '@type' => 'Collection', + ]); + } + + public function testCustomRelation(): void + { + $response = $this->get('/api/home', headers: ['accept' => ['application/ld+json']]); + $home = $response->json(); + $this->assertArrayHasKey('order', $home); + $this->assertArrayHasKey('id', $home['order']); + $this->assertArrayHasKey('number', $home['order']); + } +} diff --git a/src/Laravel/Tests/JsonProblemTest.php b/src/Laravel/Tests/JsonProblemTest.php new file mode 100644 index 00000000000..f2dc6cb09d8 --- /dev/null +++ b/src/Laravel/Tests/JsonProblemTest.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Attributes\DefineEnvironment; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; + +class JsonProblemTest extends TestCase +{ + use RefreshDatabase; + use WithWorkbench; + + public function testNotFound(): void + { + $response = $this->get('/api/books/notfound', headers: ['accept' => 'application/ld+json']); + $response->assertStatus(404); + $response->assertHeader('content-type', 'application/problem+json; charset=utf-8'); + $response->assertJsonFragment([ + '@context' => '/api/contexts/Error', + '@id' => '/api/errors/404', + '@type' => 'Error', + 'type' => '/errors/404', + 'title' => 'An error occurred', + 'status' => 404, + 'detail' => 'Not Found', + ]); + } + + /** + * @param Application $app + */ + protected function useProductionMode($app): void + { + $app['config']->set('app.debug', false); + } + + #[DefineEnvironment('useProductionMode')] + public function testProductionError(): void + { + $response = $this->post('/api/books', ['content-type' => 'application/vnd.api+json']); + $response->assertStatus(415); + $content = $response->json(); + $this->assertArrayNotHasKey('trace', $content); + $this->assertArrayNotHasKey('line', $content); + $this->assertArrayNotHasKey('file', $content); + } + + /** + * @param list}> $expected + */ + #[DefineEnvironment('useProductionMode')] + #[DataProvider('formatsProvider')] + public function testRetrieveError(string $format, string $status, array $expected): void + { + $response = $this->get('/api/errors/'.$status, ['accept' => $format]); + $this->assertEquals($expected, $response->json()); + } + + #[DefineEnvironment('useProductionMode')] + public function testRetrieveErrorHtml(): void + { + $response = $this->get('/api/errors/403', ['accept' => 'text/html']); + $this->assertEquals(' + + + + Error 403 + +

Error 403

Forbidden +', $response->getContent()); + } + + /** + * @return list}> + */ + public static function formatsProvider(): array + { + return [ + [ + 'application/vnd.api+json', + '401', + [ + 'errors' => [ + [ + 'id' => '/api/errors/401', + 'detail' => 'Unauthorized', + 'type' => 'about:blank', + 'title' => 'Error 401', + 'status' => 401, + 'code' => '401', + 'links' => [ + 'type' => 'about:blank', + ], + ], + ], + ], + ], + [ + 'application/ld+json', + '401', + [ + '@context' => '/api/contexts/Error', + '@type' => 'Error', + '@id' => '/api/errors/401', + 'detail' => 'Unauthorized', + 'title' => 'Error 401', + 'status' => 401, + 'type' => 'about:blank', + ], + ], + [ + 'application/json', + '401', + [ + 'type' => 'about:blank', + 'detail' => 'Unauthorized', + 'title' => 'Error 401', + 'status' => 401, + ], + ], + ]; + } +} diff --git a/src/Laravel/Tests/LinkHeaderTest.php b/src/Laravel/Tests/LinkHeaderTest.php new file mode 100644 index 00000000000..4aa1a41e9c5 --- /dev/null +++ b/src/Laravel/Tests/LinkHeaderTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class LinkHeaderTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('app.debug', true); + $config->set('api-platform.formats', ['jsonld' => ['application/ld+json']]); + $config->set('api-platform.docs_formats', ['jsonld' => ['application/ld+json']]); + }); + } + + public function testLinkHeader(): void + { + $response = $this->get('/api/', ['accept' => ['application/ld+json']]); + $response->assertStatus(200); + $response->assertHeader('link', '; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"'); + } +} diff --git a/src/Laravel/Tests/LinkHeaderWithoutJsonldTest.php b/src/Laravel/Tests/LinkHeaderWithoutJsonldTest.php new file mode 100644 index 00000000000..edc2178f24f --- /dev/null +++ b/src/Laravel/Tests/LinkHeaderWithoutJsonldTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class LinkHeaderWithoutJsonldTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('app.debug', true); + $config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]); + }); + } + + public function testLinkHeader(): void + { + $response = $this->get('/api/', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertHeaderMissing('link'); + } +} diff --git a/src/Laravel/Tests/Policy/BookAllowPolicy.php b/src/Laravel/Tests/Policy/BookAllowPolicy.php new file mode 100644 index 00000000000..bea0109c494 --- /dev/null +++ b/src/Laravel/Tests/Policy/BookAllowPolicy.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use Illuminate\Foundation\Auth\User; +use Workbench\App\Models\Book; + +class BookAllowPolicy +{ + /** + * Determine whether the user can view any models. + */ + public function viewAny(?User $user): bool + { + return true; + } + + /** + * Determine whether the user can view the model. + */ + public function view(?User $user, Book $book): bool + { + return true; + } + + /** + * Determine whether the user can create models. + */ + public function create(?User $user): bool + { + return true; + } + + /** + * Determine whether the user can update the model. + */ + public function update(?User $user, Book $book): bool + { + return true; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(?User $user, Book $book): bool + { + return true; + } +} diff --git a/src/Laravel/Tests/Policy/BookDenyPolicy.php b/src/Laravel/Tests/Policy/BookDenyPolicy.php new file mode 100644 index 00000000000..d9f70e82930 --- /dev/null +++ b/src/Laravel/Tests/Policy/BookDenyPolicy.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use Illuminate\Foundation\Auth\User; +use Workbench\App\Models\Book; + +class BookDenyPolicy +{ + /** + * Determine whether the user can view any models. + */ + public function viewAny(?User $user): bool + { + return false; + } + + /** + * Determine whether the user can view the model. + */ + public function view(?User $user, Book $book): bool + { + return false; + } + + /** + * Determine whether the user can create models. + */ + public function create(?User $user): bool + { + return false; + } + + /** + * Determine whether the user can update the model. + */ + public function update(?User $user, Book $book): bool + { + return false; + } + + /** + * Determine whether the user can delete the model. + */ + public function delete(?User $user, Book $book): bool + { + return false; + } +} diff --git a/src/Laravel/Tests/Policy/PolicyAllowTest.php b/src/Laravel/Tests/Policy/PolicyAllowTest.php new file mode 100644 index 00000000000..c07eeb7a459 --- /dev/null +++ b/src/Laravel/Tests/Policy/PolicyAllowTest.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Gate; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; + +class PolicyAllowTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + Gate::guessPolicyNamesUsing(function (string $modelClass) { + return Book::class === $modelClass ? + BookAllowPolicy::class : + null; + }); + + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('app.debug', true); + }); + } + + public function testGetCollection(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + } + + public function testGetEmptyCollection(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->get('/api/books?publicationDate[gt]=9999-12-31', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + $response->assertJsonFragment([ + 'meta' => [ + 'totalItems' => 0, + 'itemsPerPage' => 5, + 'currentPage' => 1, + ], + ]); + } + + public function testGetBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(200); + } + + public function testCreateBook(): void + { + AuthorFactory::new()->create(); + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'data' => [ + 'attributes' => [ + 'name' => 'Don Quichotte', + 'isbn' => fake()->isbn13(), + 'publicationDate' => fake()->optional()->date(), + ], + 'relationships' => [ + 'author' => [ + 'data' => [ + 'id' => $this->getIriFromResource($author), + 'type' => 'Author', + ], + ], + ], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + + $response->assertStatus(201); + } + + public function testUpdateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->putJson( + $iri, + [ + 'data' => ['attributes' => ['name' => 'updated title']], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + $response->assertStatus(200); + } + + public function testPatchBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->patchJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(200); + } + + public function testDeleteBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/vnd.api+json']); + $response->assertStatus(204); + $this->assertNull(Book::find($book->id)); + } +} diff --git a/src/Laravel/Tests/Policy/PolicyDenyTest.php b/src/Laravel/Tests/Policy/PolicyDenyTest.php new file mode 100644 index 00000000000..dfdf114ae41 --- /dev/null +++ b/src/Laravel/Tests/Policy/PolicyDenyTest.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Policy; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Gate; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; + +class PolicyDenyTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + Gate::guessPolicyNamesUsing(function (string $modelClass) { + return Book::class === $modelClass ? + BookDenyPolicy::class : + null; + }); + + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('api-platform.docs_formats', ['jsonapi' => ['application/vnd.api+json']]); + $config->set('app.debug', true); + }); + } + + public function testGetCollection(): void + { + $response = $this->get('/api/books', ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(403); + } + + public function testGetBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->get($iri, ['accept' => ['application/vnd.api+json']]); + $response->assertStatus(403); + } + + public function testCreateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $author = Author::find(1); + $response = $this->postJson( + '/api/books', + [ + 'data' => [ + 'attributes' => [ + 'name' => 'Don Quichotte', + 'isbn' => fake()->isbn13(), + 'publicationDate' => fake()->optional()->date(), + ], + 'relationships' => [ + 'author' => [ + 'data' => [ + 'id' => $this->getIriFromResource($author), + 'type' => 'Author', + ], + ], + ], + ], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + + $response->assertStatus(403); + } + + public function testUpdateBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->putJson( + $iri, + [ + 'data' => ['attributes' => ['name' => 'updated title']], + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/vnd.api+json', + ] + ); + $response->assertStatus(403); + } + + public function testPatchBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->patchJson( + $iri, + [ + 'name' => 'updated title', + ], + [ + 'accept' => 'application/vnd.api+json', + 'content_type' => 'application/merge-patch+json', + ] + ); + $response->assertStatus(403); + } + + public function testDeleteBook(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $iri = $this->getIriFromResource($book); + $response = $this->delete($iri, headers: ['accept' => 'application/vnd.api+json']); + $response->assertStatus(403); + } +} diff --git a/src/Laravel/Tests/PurgerTest.php b/src/Laravel/Tests/PurgerTest.php new file mode 100644 index 00000000000..2cbb695b514 --- /dev/null +++ b/src/Laravel/Tests/PurgerTest.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; +use Workbench\App\Models\Author; +use Workbench\App\Models\Book; +use Workbench\App\Purger\MockPurger; +use Workbench\Database\Factories\AuthorFactory; +use Workbench\Database\Factories\BookFactory; + +class PurgerTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + protected function getEnvironmentSetUp($app): void + { + $app['config']->set('api-platform.http_cache.invalidation.purger', MockPurger::class); + } + + protected function setUp(): void + { + parent::setUp(); + MockPurger::reset(); + } + + public function testPurgeOnCreate(): void + { + AuthorFactory::new()->create(); + $author = Author::first(); + + $r = $this->postJson('/api/books', [ + 'isbn' => '9783161484100', + 'name' => 'The Test Book', + 'author' => '/api/authors/'.$author->id, + ], ['Accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + + $this->assertTagsWerePurged([ + $r->json()['@id'], + '/api/books', + ]); + } + + public function testPurgeOnUpdate(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + + $this->patchJson('/api/books/'.$book->id, [ + 'name' => 'An Updated Name', + ], [ + 'Accept' => 'application/ld+json', + 'Content-Type' => 'application/merge-patch+json', + ]); + + $this->assertTagsWerePurged([ + '/api/books', + '/api/books/'.$book->id, + ]); + } + + public function testPurgeOnDelete(): void + { + BookFactory::new()->has(AuthorFactory::new())->create(); + $book = Book::first(); + $this->delete('/api/books/'.$book->id, headers: ['accept' => 'application/ld+json']); + + $this->assertTagsWerePurged([ + '/api/books', + '/api/books/'.$book->id, + ]); + } + + /** + * @param string[] $expectedTags + */ + private function assertTagsWerePurged(array $expectedTags): void + { + sort($expectedTags); + $this->assertEquals($expectedTags, MockPurger::getPurgedTags()); + } +} diff --git a/src/Laravel/Tests/SnakeCaseApiTest.php b/src/Laravel/Tests/SnakeCaseApiTest.php new file mode 100644 index 00000000000..2efa56c0835 --- /dev/null +++ b/src/Laravel/Tests/SnakeCaseApiTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Str; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class SnakeCaseApiTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.name_converter', null); + $config->set('api-platform.formats', ['jsonld' => ['application/ld+json']]); + $config->set('api-platform.docs_formats', ['jsonld' => ['application/ld+json']]); + }); + } + + public function testRelationIsHandledOnCreateWithNestedDataSnakeCase(): void + { + $cartData = [ + 'product_sku' => 'SKU_TEST_001', + 'quantity' => 2, + 'price_at_addition' => '19.99', + 'shopping_cart' => [ + 'user_identifier' => 'user-'.Str::uuid()->toString(), + 'status' => 'active', + ], + ]; + + $response = $this->postJson('/api/cart_items', $cartData, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + $response->assertStatus(201); + + $response + ->assertJson([ + '@context' => '/api/contexts/CartItem', + '@id' => '/api/cart_items/1', + '@type' => 'CartItem', + 'id' => 1, + 'product_sku' => 'SKU_TEST_001', + 'quantity' => 2, + 'price_at_addition' => 19.99, + 'shopping_cart' => [ + '@id' => '/api/shopping_carts/1', + '@type' => 'ShoppingCart', + 'user_identifier' => $cartData['shopping_cart']['user_identifier'], + 'status' => 'active', + ], + ]); + } + + public function testFailWithCamelCase(): void + { + $cartData = [ + 'productSku' => 'SKU_TEST_001', + 'quantity' => 2, + 'priceAtAddition' => '19.99', + 'shoppingCart' => [ + 'userIdentifier' => 'user-'.Str::uuid()->toString(), + 'status' => 'active', + ], + ]; + + $response = $this->postJson('/api/cart_items', $cartData, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + $response->assertStatus(422); + } +} diff --git a/src/Laravel/Tests/Unit/State/ItemProviderTest.php b/src/Laravel/Tests/Unit/State/ItemProviderTest.php new file mode 100644 index 00000000000..5ab749a7ff4 --- /dev/null +++ b/src/Laravel/Tests/Unit/State/ItemProviderTest.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests\Unit\State; + +use ApiPlatform\Laravel\Eloquent\Extension\QueryExtensionInterface; +use ApiPlatform\Laravel\Eloquent\State\ItemProvider; +use ApiPlatform\Laravel\Eloquent\State\LinksHandlerInterface; +use ApiPlatform\Metadata\Get; +use Orchestra\Testbench\TestCase; +use Psr\Container\ContainerInterface; +use Workbench\App\Models\Book; + +class ItemProviderTest extends TestCase +{ + public function testItemProviderWithQueryExtension(): void + { + $linksHandler = $this->createMock(LinksHandlerInterface::class); + $handleLinksLocator = $this->createMock(ContainerInterface::class); + $queryExtension = $this->createMock(QueryExtensionInterface::class); + $queryExtension->expects($this->once())->method('apply')->willReturnArgument(0); + + $queryExtensions = [$queryExtension]; + $itemProvider = new ItemProvider($linksHandler, $handleLinksLocator, $queryExtensions); + + $operation = new Get(class: Book::class); + $itemProvider->provide($operation); + } +} diff --git a/src/Laravel/Tests/ValidationTest.php b/src/Laravel/Tests/ValidationTest.php new file mode 100644 index 00000000000..77f50562d36 --- /dev/null +++ b/src/Laravel/Tests/ValidationTest.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Laravel\Tests; + +use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Foundation\Testing\RefreshDatabase; +use Orchestra\Testbench\Concerns\WithWorkbench; +use Orchestra\Testbench\TestCase; + +class ValidationTest extends TestCase +{ + use ApiTestAssertionsTrait; + use RefreshDatabase; + use WithWorkbench; + + /** + * @param Application $app + */ + protected function defineEnvironment($app): void + { + tap($app['config'], function (Repository $config): void { + $config->set('api-platform.formats', ['jsonld' => ['application/ld+json']]); + $config->set('api-platform.docs_formats', ['jsonld' => ['application/ld+json']]); + }); + } + + public function testValidationCamelCase(): void + { + $data = [ + 'surName' => '', + ]; + + $response = $this->postJson('/api/issue_6932', $data, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + $response->assertJsonFragment(['violations' => [['propertyPath' => 'surName', 'message' => 'The sur name field is required.']]]); // validate that the name has been converted + $response->assertStatus(422); + } + + public function testCamelCaseValid(): void + { + $data = [ + 'surName' => 'ok', + ]; + + $response = $this->postJson('/api/issue_6932', $data, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + $response->assertStatus(201); + } + + public function testValidationSnakeCase(): void + { + $data = [ + 'sur_name' => 'test', + ]; + + $response = $this->postJson('/api/issue_6932', $data, ['accept' => 'application/ld+json', 'content-type' => 'application/ld+json']); + $response->assertStatus(422); + } + + public function testRouteWithRequirements(): void + { + $response = $this->get('api/issue_7194_requirements/test', ['accept' => 'application/ld+json']); + $response->assertStatus(404); + $response = $this->get('api/issue_7194_requirements/1', ['accept' => 'application/ld+json']); + $response->assertStatus(200); + } + + public function testGetCollectionWithFormRequestValidation(): void + { + $response = $this->get('/api/slots/dropoff', ['accept' => 'application/ld+json']); + $response->assertStatus(422); + $response->assertJsonFragment(['violations' => [ + ['propertyPath' => 'pickupDate', 'message' => 'The pickup date field is required.'], + ['propertyPath' => 'pickupSlotId', 'message' => 'The pickup slot id field is required.'], + ]]); + } +} diff --git a/src/Laravel/composer.json b/src/Laravel/composer.json new file mode 100644 index 00000000000..fbc5c5917a9 --- /dev/null +++ b/src/Laravel/composer.json @@ -0,0 +1,137 @@ +{ + "name": "api-platform/laravel", + "description": "API Platform support for Laravel", + "keywords": [ + "Laravel", + "REST", + "GraphQL", + "API", + "JSON-LD", + "Hydra", + "JSONAPI", + "OpenAPI", + "HAL", + "Swagger" + ], + "homepage": "https://api-platform.com/", + "license": "MIT", + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr/" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "require": { + "php": ">=8.2", + "api-platform/documentation": "^4.1.11", + "api-platform/hydra": "^4.1.11", + "api-platform/json-api": "^4.1.11", + "api-platform/hal": "^4.2.0-beta.1", + "api-platform/json-schema": "^4.1.11", + "api-platform/jsonld": "^4.1.11", + "api-platform/metadata": "^4.1.11", + "api-platform/openapi": "^4.1.11", + "api-platform/serializer": "^4.1.11", + "api-platform/state": "^4.1.11", + "illuminate/config": "^11.0 || ^12.0", + "illuminate/container": "^11.0 || ^12.0", + "illuminate/contracts": "^11.0 || ^12.0", + "illuminate/database": "^11.0 || ^12.0", + "illuminate/http": "^11.0 || ^12.0", + "illuminate/pagination": "^11.0 || ^12.0", + "illuminate/routing": "^11.0 || ^12.0", + "illuminate/support": "^11.0 || ^12.0", + "laravel/framework": "^11.0 || ^12.0", + "symfony/deprecation-contracts": "^3.6", + "symfony/type-info": "^7.2", + "symfony/web-link": "^6.4 || ^7.1", + "willdurand/negotiation": "^3.1" + }, + "require-dev": { + "api-platform/graphql": "^4.1", + "api-platform/http-cache": "^4.1", + "doctrine/dbal": "^4.0", + "larastan/larastan": "^2.0 || ^3.0", + "laravel/sanctum": "^4.0", + "orchestra/testbench": "^9.1", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.29 || ^2.0", + "phpunit/phpunit": "11.5.x-dev", + "symfony/http-client": "^7.3" + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Laravel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/", + "/workbench/" + ] + }, + "config": { + "sort-packages": true + }, + "suggest": { + "api-platform/graphql": "Enable GraphQl support.", + "api-platform/http-cache": "Enable HTTP Cache support.", + "phpstan/phpdoc-parser": "To support extracting metadata from PHPDoc." + }, + "extra": { + "laravel": { + "providers": [ + "ApiPlatform\\Laravel\\ApiPlatformProvider", + "ApiPlatform\\Laravel\\ApiPlatformDeferredProvider", + "ApiPlatform\\Laravel\\Eloquent\\ApiPlatformEventProvider" + ] + }, + "branch-alias": { + "dev-main": "4.3.x-dev", + "dev-4.2": "4.2.x-dev", + "dev-3.4": "3.4.x-dev", + "dev-4.1": "4.1.x-dev" + }, + "symfony": { + "require": "^6.4 || ^7.1" + }, + "thanks": { + "name": "api-platform/api-platform", + "url": "https://github.com/api-platform/api-platform" + } + }, + "autoload-dev": { + "psr-4": { + "Workbench\\App\\": "workbench/app/", + "Workbench\\Database\\Factories\\": "workbench/database/factories/", + "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" + } + }, + "scripts": { + "build": "@php vendor/bin/testbench workbench:build --ansi", + "test": "@php vendor/bin/testbench package:test", + "post-autoload-dump": [ + "@clear", + "@prepare" + ], + "clear": "@php vendor/bin/testbench package:purge-skeleton --ansi", + "prepare": "@php vendor/bin/testbench package:discover --ansi", + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@build", + "@php vendor/bin/testbench serve --ansi" + ], + "lint": [ + "@php vendor/bin/phpstan analyse --verbose --ansi" + ] + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/soyuka/phpunit" + } + ] +} diff --git a/src/Laravel/config/api-platform.php b/src/Laravel/config/api-platform.php new file mode 100644 index 00000000000..ecc875fb808 --- /dev/null +++ b/src/Laravel/config/api-platform.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +use ApiPlatform\Metadata\UrlGeneratorInterface; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Auth\AuthenticationException; +use Symfony\Component\Serializer\NameConverter\SnakeCaseToCamelCaseNameConverter; + +return [ + 'title' => 'API Platform', + 'description' => 'My awesome API', + 'version' => '1.0.0', + 'show_webby' => true, + + 'routes' => [ + 'domain' => null, + // Global middleware applied to every API Platform routes + // 'middleware' => [], + ], + + 'resources' => [ + app_path('Models'), + ], + + 'formats' => [ + 'jsonld' => ['application/ld+json'], + // 'jsonapi' => ['application/vnd.api+json'], + // 'csv' => ['text/csv'], + ], + + 'patch_formats' => [ + 'json' => ['application/merge-patch+json'], + ], + + 'docs_formats' => [ + 'jsonld' => ['application/ld+json'], + // 'jsonapi' => ['application/vnd.api+json'], + 'jsonopenapi' => ['application/vnd.openapi+json'], + 'html' => ['text/html'], + ], + + 'error_formats' => [ + 'jsonproblem' => ['application/problem+json'], + ], + + 'defaults' => [ + 'pagination_enabled' => true, + 'pagination_partial' => false, + 'pagination_client_enabled' => false, + 'pagination_client_items_per_page' => false, + 'pagination_client_partial' => false, + 'pagination_items_per_page' => 30, + 'pagination_maximum_items_per_page' => 30, + 'route_prefix' => '/api', + 'middleware' => [], + ], + + 'pagination' => [ + 'page_parameter_name' => 'page', + 'enabled_parameter_name' => 'pagination', + 'items_per_page_parameter_name' => 'itemsPerPage', + 'partial_parameter_name' => 'partial', + ], + + 'graphql' => [ + 'enabled' => false, + 'nesting_separator' => '__', + 'introspection' => ['enabled' => true], + 'max_query_complexity' => 500, + 'max_query_depth' => 200, + // 'middleware' => null, + ], + + 'graphiql' => [ + // 'enabled' => true, + // 'domain' => null, + // 'middleware' => null, + ], + + // set to null if you want to keep snake_case + 'name_converter' => SnakeCaseToCamelCaseNameConverter::class, + + 'exception_to_status' => [ + AuthenticationException::class => 401, + AuthorizationException::class => 403, + ], + + 'swagger_ui' => [ + 'enabled' => true, + // 'apiKeys' => [ + // 'api' => [ + // 'name' => 'Authorization', + // 'type' => 'header', + // ], + // ], + // 'oauth' => [ + // 'enabled' => true, + // 'type' => 'oauth2', + // 'flow' => 'authorizationCode', + // 'tokenUrl' => '', + // 'authorizationUrl' =>'', + // 'refreshUrl' => '', + // 'scopes' => ['scope1' => 'Description scope 1'], + // 'pkce' => true, + // ], + // 'license' => [ + // 'name' => 'Apache 2.0', + // 'url' => 'https://www.apache.org/licenses/LICENSE-2.0.html', + // ], + // 'contact' => [ + // 'name' => 'API Support', + // 'url' => 'https://www.example.com/support', + // 'email' => 'support@example.com', + // ], + // 'http_auth' => [ + // 'Personal Access Token' => [ + // 'scheme' => 'bearer', + // 'bearerFormat' => 'JWT', + // ], + // ], + ], + + // 'openapi' => [ + // 'tags' => [], + // ], + + 'url_generation_strategy' => UrlGeneratorInterface::ABS_PATH, + + 'serializer' => [ + 'hydra_prefix' => false, + // 'datetime_format' => \DateTimeInterface::RFC3339, + ], + + // we recommend using "file" or "acpu" + 'cache' => 'file', + + // install `api-platform/http-cache` + // 'http_cache' => [ + // 'etag' => false, + // 'max_age' => null, + // 'shared_max_age' => null, + // 'vary' => null, + // 'public' => null, + // 'stale_while_revalidate' => null, + // 'stale_if_error' => null, + // 'invalidation' => [ + // 'urls' => [], + // 'scoped_clients' => [], + // 'max_header_length' => 7500, + // 'request_options' => [], + // 'purger' => ApiPlatform\HttpCache\SouinPurger::class, + // ], + // ], +]; diff --git a/src/Laravel/phpstan.neon.dist b/src/Laravel/phpstan.neon.dist new file mode 100644 index 00000000000..0e2effea271 --- /dev/null +++ b/src/Laravel/phpstan.neon.dist @@ -0,0 +1,20 @@ +includes: + - vendor/larastan/larastan/extension.neon + +parameters: + parallel: + maximumNumberOfProcesses: 1 + level: 7 + paths: + - ApiResource + - config + - routes + - Controller + - Eloquent + - Exception + - Routing + - State + - Test + - Tests + ignoreErrors: + - '#Cannot call method expectsQuestion#' diff --git a/src/Laravel/phpunit.xml.dist b/src/Laravel/phpunit.xml.dist new file mode 100644 index 00000000000..13ed0e2c01d --- /dev/null +++ b/src/Laravel/phpunit.xml.dist @@ -0,0 +1,32 @@ + + + + + + + + + ./Tests/ + + + + + + trigger_deprecation + + + ./ + + + ./Tests + ./vendor + + + diff --git a/src/Laravel/public/400.css b/src/Laravel/public/400.css new file mode 100644 index 00000000000..4a3ee47e66f --- /dev/null +++ b/src/Laravel/public/400.css @@ -0,0 +1,79 @@ +/* open-sans-cyrillic-ext-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(https://github.com/files/open-sans-cyrillic-ext-400-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-cyrillic-ext-400-normal.woff) format('woff'); + unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; +} + +/* open-sans-cyrillic-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(https://github.com/files/open-sans-cyrillic-400-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-cyrillic-400-normal.woff) format('woff'); + unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; +} + +/* open-sans-greek-ext-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(https://github.com/files/open-sans-greek-ext-400-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-greek-ext-400-normal.woff) format('woff'); + unicode-range: U+1F00-1FFF; +} + +/* open-sans-greek-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(https://github.com/files/open-sans-greek-400-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-greek-400-normal.woff) format('woff'); + unicode-range: U+0370-03FF; +} + +/* open-sans-hebrew-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(https://github.com/files/open-sans-hebrew-400-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-hebrew-400-normal.woff) format('woff'); + unicode-range: U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F; +} + +/* open-sans-vietnamese-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(https://github.com/files/open-sans-vietnamese-400-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-vietnamese-400-normal.woff) format('woff'); + unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; +} + +/* open-sans-latin-ext-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(https://github.com/files/open-sans-latin-ext-400-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-latin-ext-400-normal.woff) format('woff'); + unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; +} + +/* open-sans-latin-400-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 400; + src: url(https://github.com/files/open-sans-latin-400-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-latin-400-normal.woff) format('woff'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} \ No newline at end of file diff --git a/src/Laravel/public/700.css b/src/Laravel/public/700.css new file mode 100644 index 00000000000..f20bbcf0684 --- /dev/null +++ b/src/Laravel/public/700.css @@ -0,0 +1,79 @@ +/* open-sans-cyrillic-ext-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(https://github.com/files/open-sans-cyrillic-ext-700-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-cyrillic-ext-700-normal.woff) format('woff'); + unicode-range: U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; +} + +/* open-sans-cyrillic-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(https://github.com/files/open-sans-cyrillic-700-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-cyrillic-700-normal.woff) format('woff'); + unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; +} + +/* open-sans-greek-ext-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(https://github.com/files/open-sans-greek-ext-700-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-greek-ext-700-normal.woff) format('woff'); + unicode-range: U+1F00-1FFF; +} + +/* open-sans-greek-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(https://github.com/files/open-sans-greek-700-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-greek-700-normal.woff) format('woff'); + unicode-range: U+0370-03FF; +} + +/* open-sans-hebrew-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(https://github.com/files/open-sans-hebrew-700-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-hebrew-700-normal.woff) format('woff'); + unicode-range: U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F; +} + +/* open-sans-vietnamese-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(https://github.com/files/open-sans-vietnamese-700-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-vietnamese-700-normal.woff) format('woff'); + unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; +} + +/* open-sans-latin-ext-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(https://github.com/files/open-sans-latin-ext-700-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-latin-ext-700-normal.woff) format('woff'); + unicode-range: U+0100-02AF,U+0304,U+0308,U+0329,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF; +} + +/* open-sans-latin-700-normal */ +@font-face { + font-family: 'Open Sans'; + font-style: normal; + font-display: swap; + font-weight: 700; + src: url(https://github.com/files/open-sans-latin-700-normal.woff2) format('woff2'), url(https://github.com/files/open-sans-latin-700-normal.woff) format('woff'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} \ No newline at end of file diff --git a/src/Laravel/public/car.svg b/src/Laravel/public/car.svg new file mode 100644 index 00000000000..b74b16f85fb --- /dev/null +++ b/src/Laravel/public/car.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Laravel/public/es6-promise/es6-promise.auto.min.js b/src/Laravel/public/es6-promise/es6-promise.auto.min.js new file mode 100644 index 00000000000..fdf8bff2676 --- /dev/null +++ b/src/Laravel/public/es6-promise/es6-promise.auto.min.js @@ -0,0 +1 @@ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.ES6Promise=e()}(this,function(){"use strict";function t(t){var e=typeof t;return null!==t&&("object"===e||"function"===e)}function e(t){return"function"==typeof t}function n(t){B=t}function r(t){G=t}function o(){return function(){return process.nextTick(a)}}function i(){return"undefined"!=typeof z?function(){z(a)}:c()}function s(){var t=0,e=new J(a),n=document.createTextNode("");return e.observe(n,{characterData:!0}),function(){n.data=t=++t%2}}function u(){var t=new MessageChannel;return t.port1.onmessage=a,function(){return t.port2.postMessage(0)}}function c(){var t=setTimeout;return function(){return t(a,1)}}function a(){for(var t=0;t this.readyToRead) { + push = USE_ALLOC ? Buffer.alloc(this.readyToRead, '', 'binary') : new Buffer(this.readyToRead, 'binary'); + this.responseBuffer.copy(push, 0, 0, this.readyToRead); + restSize = this.responseBuffer.length - this.readyToRead; + rest = USE_ALLOC ? Buffer.alloc(restSize, '', 'binary') : new Buffer(restSize, 'binary'); + this.responseBuffer.copy(rest, 0, this.readyToRead); + } else { + push = this.responseBuffer; + rest = USE_ALLOC ? Buffer.alloc(0, '', 'binary') : new Buffer(0, 'binary'); + } + this.responseBuffer = rest; + this.readyToRead = 0; + if (this.options.encoding) { + this.push(push, this.options.encoding); + } else { + this.push(push); + } +}; + +FetchStream.prototype.destroy = function (ex) { + this.emit('destroy', ex); +}; + +FetchStream.prototype.normalizeOptions = function () { + + // cookiejar + this.cookieJar = this.options.cookieJar || new CookieJar(); + + // default redirects - 10 + // if disableRedirect is set, then 0 + if (!this.options.disableRedirect && typeof this.options.maxRedirects !== 'number' && + !(this.options.maxRedirects instanceof Number)) { + this.options.maxRedirects = 10; + } else if (this.options.disableRedirects) { + this.options.maxRedirects = 0; + } + + // normalize header keys + // HTTP and HTTPS takes in key names in case insensitive but to find + // an exact value from an object key name needs to be case sensitive + // so we're just lowercasing all input keys + this.options.headers = this.options.headers || {}; + + var keys = Object.keys(this.options.headers); + var newheaders = {}; + var i; + + for (i = keys.length - 1; i >= 0; i--) { + newheaders[keys[i].toLowerCase().trim()] = this.options.headers[keys[i]]; + } + + this.options.headers = newheaders; + + if (!this.options.headers['user-agent']) { + this.options.headers['user-agent'] = this.userAgent; + } + + if (!this.options.headers.pragma) { + this.options.headers.pragma = 'no-cache'; + } + + if (!this.options.headers['cache-control']) { + this.options.headers['cache-control'] = 'no-cache'; + } + + if (!this.options.disableGzip) { + this.options.headers['accept-encoding'] = 'gzip, deflate'; + } else { + delete this.options.headers['accept-encoding']; + } + + // max length for the response, + // if not set, default is Infinity + if (!this.options.maxResponseLength) { + this.options.maxResponseLength = Infinity; + } + + // method: + // defaults to GET, or when payload present to POST + if (!this.options.method) { + this.options.method = this.options.payload || this.options.payloadSize ? 'POST' : 'GET'; + } + + // set cookies + // takes full cookie definition strings as params + if (this.options.cookies) { + for (i = 0; i < this.options.cookies.length; i++) { + this.cookieJar.setCookie(this.options.cookies[i], this.url); + } + } + + // rejectUnauthorized + if (typeof this.options.rejectUnauthorized === 'undefined') { + this.options.rejectUnauthorized = true; + } +}; + +FetchStream.prototype.parseUrl = function (url) { + var urlparts = urllib.parse(url, false, true), + transport, + urloptions = { + host: urlparts.hostname || urlparts.host, + port: urlparts.port, + path: urlparts.pathname + (urlparts.search || '') || '/', + method: this.options.method, + rejectUnauthorized: this.options.rejectUnauthorized + }; + + switch (urlparts.protocol) { + case 'https:': + transport = https; + break; + case 'http:': + default: + transport = http; + break; + } + + if (transport === https) { + if('agentHttps' in this.options){ + urloptions.agent = this.options.agentHttps; + } + if('agent' in this.options){ + urloptions.agent = this.options.agent; + } + } else { + if('agentHttp' in this.options){ + urloptions.agent = this.options.agentHttp; + } + if('agent' in this.options){ + urloptions.agent = this.options.agent; + } + } + + if (!urloptions.port) { + switch (urlparts.protocol) { + case 'https:': + urloptions.port = 443; + break; + case 'http:': + default: + urloptions.port = 80; + break; + } + } + + urloptions.headers = this.options.headers || {}; + + if (urlparts.auth) { + var buf = USE_ALLOC ? Buffer.alloc(Buffer.byteLength(urlparts.auth), urlparts.auth) : new Buffer(urlparts.auth); + urloptions.headers.Authorization = 'Basic ' + buf.toString('base64'); + } + + return { + urloptions: urloptions, + transport: transport + }; +}; + +FetchStream.prototype.setEncoding = function (encoding) { + this.options.encoding = encoding; +}; + +FetchStream.prototype.runStream = function (url) { + var url_data = this.parseUrl(url), + cookies = this.cookieJar.getCookies(url); + + if (cookies) { + url_data.urloptions.headers.cookie = cookies; + } else { + delete url_data.urloptions.headers.cookie; + } + + if (this.options.payload) { + url_data.urloptions.headers['content-length'] = Buffer.byteLength(this.options.payload || '', 'utf-8'); + } + + if (this.options.payloadSize) { + url_data.urloptions.headers['content-length'] = this.options.payloadSize; + } + + if (this.options.asyncDnsLoookup) { + var dnsCallback = (function (err, addresses) { + if (err) { + this.emit('error', err); + return; + } + + url_data.urloptions.headers.host = url_data.urloptions.hostname || url_data.urloptions.host; + url_data.urloptions.hostname = addresses[0]; + url_data.urloptions.host = url_data.urloptions.headers.host + (url_data.urloptions.port ? ':' + url_data.urloptions.port : ''); + + this._runStream(url_data, url); + }).bind(this); + + if (net.isIP(url_data.urloptions.host)) { + dnsCallback(null, [url_data.urloptions.host]); + } else { + dns.resolve4(url_data.urloptions.host, dnsCallback); + } + } else { + this._runStream(url_data, url); + } +}; + +FetchStream.prototype._runStream = function (url_data, url) { + + var req = url_data.transport.request(url_data.urloptions, (function (res) { + + // catch new cookies before potential redirect + if (Array.isArray(res.headers['set-cookie'])) { + for (var i = 0; i < res.headers['set-cookie'].length; i++) { + this.cookieJar.setCookie(res.headers['set-cookie'][i], url); + } + } + + if ([301, 302, 303, 307, 308].indexOf(res.statusCode) >= 0) { + if (!this.options.disableRedirects && this.options.maxRedirects > this._redirect_count && res.headers.location) { + this._redirect_count++; + req.destroy(); + this.runStream(urllib.resolve(url, res.headers.location)); + return; + } + } + + this.meta = { + status: res.statusCode, + responseHeaders: res.headers, + finalUrl: url, + redirectCount: this._redirect_count, + cookieJar: this.cookieJar + }; + + var curlen = 0, + maxlen, + + receive = (function (chunk) { + if (curlen + chunk.length > this.options.maxResponseLength) { + maxlen = this.options.maxResponseLength - curlen; + } else { + maxlen = chunk.length; + } + + if (maxlen <= 0) { + return; + } + + curlen += Math.min(maxlen, chunk.length); + if (maxlen >= chunk.length) { + if (this.responseBuffer.length === 0) { + this.responseBuffer = chunk; + } else { + this.responseBuffer = Buffer.concat([this.responseBuffer, chunk]); + } + } else { + this.responseBuffer = Buffer.concat([this.responseBuffer, chunk], this.responseBuffer.length + maxlen); + } + this.drainBuffer(); + }).bind(this), + + error = (function (e) { + this.ended = true; + this.emit('error', e); + this.drainBuffer(); + }).bind(this), + + end = (function () { + this.ended = true; + if (this.responseBuffer.length === 0) { + this.push(null); + } + }).bind(this), + + unpack = (function (type, res) { + var z = zlib['create' + type](); + z.on('data', receive); + z.on('error', error); + z.on('end', end); + res.pipe(z); + }).bind(this); + + this.emit('meta', this.meta); + + if (res.headers['content-encoding']) { + switch (res.headers['content-encoding'].toLowerCase().trim()) { + case 'gzip': + return unpack('Gunzip', res); + case 'deflate': + return unpack('InflateRaw', res); + } + } + + res.on('data', receive); + res.on('end', end); + + }).bind(this)); + + req.on('error', (function (e) { + this.emit('error', e); + }).bind(this)); + + if (this.options.timeout) { + req.setTimeout(this.options.timeout, req.abort.bind(req)); + } + this.on('destroy', req.abort.bind(req)); + + if (this.options.payload) { + req.end(this.options.payload); + } else if (this.options.payloadStream) { + this.options.payloadStream.pipe(req); + this.options.payloadStream.resume(); + } else { + req.end(); + } +}; + +function fetchUrl(url, options, callback) { + if (!callback && typeof options === 'function') { + callback = options; + options = undefined; + } + options = options || {}; + + var fetchstream = new FetchStream(url, options), + response_data, chunks = [], + length = 0, + curpos = 0, + buffer, + content_type, + callbackFired = false; + + fetchstream.on('meta', function (meta) { + response_data = meta; + content_type = _parseContentType(meta.responseHeaders['content-type']); + }); + + fetchstream.on('data', function (chunk) { + if (chunk) { + chunks.push(chunk); + length += chunk.length; + } + }); + + fetchstream.on('error', function (error) { + if (error && error.code === 'HPE_INVALID_CONSTANT') { + // skip invalid formatting errors + return; + } + if (callbackFired) { + return; + } + callbackFired = true; + callback(error); + }); + + fetchstream.on('end', function () { + if (callbackFired) { + return; + } + callbackFired = true; + + buffer = USE_ALLOC ? Buffer.alloc(length) : new Buffer(length); + for (var i = 0, len = chunks.length; i < len; i++) { + chunks[i].copy(buffer, curpos); + curpos += chunks[i].length; + } + + if (content_type.mimeType === 'text/html') { + content_type.charset = _findHTMLCharset(buffer) || content_type.charset; + } + + content_type.charset = (options.overrideCharset || content_type.charset || 'utf-8').trim().toLowerCase(); + + + if (!options.disableDecoding && !content_type.charset.match(/^utf-?8$/i)) { + buffer = encodinglib.convert(buffer, 'UTF-8', content_type.charset); + } + + if (options.outputEncoding) { + return callback(null, response_data, buffer.toString(options.outputEncoding)); + } else { + return callback(null, response_data, buffer); + } + + }); +} + +function _parseContentType(str) { + if (!str) { + return {}; + } + var parts = str.split(';'), + mimeType = parts.shift(), + charset, chparts; + + for (var i = 0, len = parts.length; i < len; i++) { + chparts = parts[i].split('='); + if (chparts.length > 1) { + if (chparts[0].trim().toLowerCase() === 'charset') { + charset = chparts[1]; + } + } + } + + return { + mimeType: (mimeType || '').trim().toLowerCase(), + charset: (charset || 'UTF-8').trim().toLowerCase() // defaults to UTF-8 + }; +} + +function _findHTMLCharset(htmlbuffer) { + + var body = htmlbuffer.toString('ascii'), + input, meta, charset; + + if ((meta = body.match(/]*?>/i))) { + input = meta[0]; + } + + if (input) { + charset = input.match(/charset\s?=\s?([a-zA-Z\-0-9]*);?/); + if (charset) { + charset = (charset[1] || '').trim().toLowerCase(); + } + } + + if (!charset && (meta = body.match(/li.selected, +#graphiql .graphiql-container .toolbar-menu-items>li.hover, +#graphiql.graphiql-container .toolbar-menu-items>li:active, +#graphiql .graphiql-container .toolbar-menu-items>li:hover, +#graphiql.graphiql-container .toolbar-select-options>li.hover, +#graphiql .graphiql-container .toolbar-select-options>li:active, +#graphiql .graphiql-container .toolbar-select-options>li:hover, +#graphiql .graphiql-container .history-contents>p:hover, +#graphiql .graphiql-container .history-contents>p:active { + background: #288690; +} diff --git a/src/Laravel/public/graphiql/graphiql.css b/src/Laravel/public/graphiql/graphiql.css new file mode 100644 index 00000000000..3083afe5043 --- /dev/null +++ b/src/Laravel/public/graphiql/graphiql.css @@ -0,0 +1,2255 @@ +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAAMwAA4AAAAABZgAAALdAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoI4ghsLEAABNgIkAxwEIAWDCgcgG3YEyI7DdHsjE9IUV+CFDh74vPL9/MmgO0un0soqjWt7En2kQoCMtXsRxyxkMqP9iO6NfSiUaLJuoRIKnhI0+ImbcWOB5XOAFVmCgxZQQmuBJRhZtsUCXm/492Dyuk2YZJdkdApZeOzyEQgKOwDgRjASBEEBVmAlgACtOHEhpjLyyrACMAB0vaLa6cAw5bc5bvhA2uwO7zXAyKPmkYNnAJgBxLEMDxFLqVBPI6EQ/daTr/QOAgfCngRoZc4UZiL623qCkf/oHVsfRCOuAIbJyF4ajQQKQLmQhNBAA4aygH9b19Xw4iAC8DkKM6WrYw/ABMAOWEAamA7sgBWACgAUSlc3SCmlc95o45idYD92Qt/+5gF19v3FALtB9+7dq/h6/Ljyu/zzYfnngwdlHxO+k39nOcO/e7nPf2vCoo3HVlmNTdnWwW3JZffuVU6cQX14kb3qUGOOJ+mjP9iMeb1Nivq5gXpJUWm+cmVK56e6PjI2uce23hHlG48vyDvym5/5q+wbkjq90rN+z53D6zXqmVUPVshZoVtrZgc4vleS1NNrni6VR8I/vTrpzpPwu1+1Pel4xBIzK16W3KcLNnVGl2RGZHbPXBAvhw4M02Ci/t0BBfw/p79XS9V7CKAMF0++DK9rtI/7MXvGATjz0TEA4K4oef476t9dS555BAoLBYCA6ei/FSzVgvg/cIR45gpTaLWeLiB+oa4xJuTks7r7/xwCmCzlpoJKALCDQmkyEsCsN0mELUADghGsGgAF6c9IXkabDYyqg6WMkZd9z7BT5gaphhhqnOH66aOvkTQhggQLpsk0xBB9DNSLJttgPQTQJBtoIE0JEY2wb+1lhF6GG62XngKUGKLFECMNkW2kZgP10+M31GZUwfojwkU0uAcQkISKFNtqGMlau3vIjjRUjMANjYkDNKeouYh7CRBmuD4CHQgHG6GXET8oT7ZU6QqUStddiABBJPSv6P315AAA) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAANUAA4AAAAABbwAAAMBAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoI0ghgLEAABNgIkAxwEIAWDMgcgG5sECK4GbGM62A+KOMNGmZWUwcdhKI9l4Sh/WwYP/3af9w0W4ERa2bOg405uoSptTooGKkF8HniO5b+Iojvye4dReBbNtVHwcLQTG2gBzQfYOqjJ/XYU/jItwgxa4I3czM4Fj9LAAnlHz+dzgSO71Jqn2QML8H66dROj0qAFLYnRhtm0b89/erW/v8l/LA6we9gCizDBtQzSf4EtkcwDT6RtmgYEQXnDKGQslZyX/CkQSFgBAE4ERggEAgmwACwQgADMsONAJKVkFWEBgAJgwMz1NlLWec3G+jtZu+rXO1i7rx/sZi0AEwB5WVY28FUE1CORQAjvtSPftAwCQQjGAbTUfm4qwrvbNmDEf5pjR4JoxElAiYiMWjQyIAEy4EBGAA4UNKCgIMC7a5Cej2sCAA+SMEEyYA2AMQBWgCmQAObACrAAQAUAJCSDMEDmo7CztfXoRGu7SUeVdbvosOq6N6PHnZ2yf9l3eXPj/q2qXdkjBL+qrix1cYsqzItOvXfRPaMXkUvPeFWoxr7tZB8gfxIhMauBapmSUhO8d3O8wUt0MoI7UAxLzt0/zhCwJnVHrsPYXenm8suPeLYORWqn/3wwK6Qp+frDiYGvxHSXFzoXfpihfmlODl9oFbOqKa8nXbZgd6axNivh4JS8xEZKChij/nuDBPx/MrxQA/WBACCtK44947xa66g/k0YcALjxaesDuBuQP/7x/3bTwmQACVMkAAQYd/7HYBqK1H97hriqWIzlN7cD8Qu1mY6Ql7eR9v8qAcCY/apKqAgArEBCCmOEAExoJiOUENTgBAI3NSBhwSjIbLboV0Blo3PIiN06hxVFfmrr0WtMvzYtWg3SBPDjz58mVY8eLTrpNOm6NfKhidepk6ZAbgbym+oG6PoN0zXxUaBHgx6Demiy6Zq0GdIl3aB6ndo04r7WvSV0/Qa0Nd2+yKcNFCrSvh/6dNKO3xV33aBeEXxNZKTyQUaverfOR49+LZno1XUboBt4oSzpEiXLUSjZDgF8+JHBMIY0KQAA) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: italic; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAALsAA4AAAAABWAAAAKbAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoIYgXsLEAABNgIkAxwEIAWCdAcgG0AEAB6HcYyyEjO2Dy0eKLv4XvfsrGs+wIhEBOHOERRRTI2158fc/aln0WYmSJq8uTRSIgUyIVMqpfa/7uYHCqzWDuHREj0f5UuuL+ZAokTaYgiIs5sF5aUutjO7QhBlgMaYvCAIIqqoCggoq0+HjRlX70MGclDLyR3Z8fb0q/ectzCv30obmLesvO5hBhRhcp7kToaLpaRXpL0htKmb5C3rIgzUIwA1fnqrhHSbqXhA3v+sK1wRtcWuhdyg9E5tGXERkaAhroCGeNqCnJxAm6m1Sb58SICvFhXFWnVAAWQoYRjYADJUQQqIYm0uSZKkfpYv1sv21dm9b7kWbV6i3BQ2Z/sOf/hl+ezXH88LRz75pnLuq4/MO/Zx+eyHc3x9VDn3yfx9n1ILyusq3ps75y90fVZ657PJ2iXgF+odHbvzv7Lrm+uTsPR0WJqYcelN7180rHDDnbeWbrx0QHht49uXjCzffOsd5RsvGvHe4yF5o+Ej97/ZMP62+Z+3Wz/08CtZ/FezhpdvG/nb6PMhC9vNvHFx3Du9X47etewROuONg4L0v2eI+L9X7dt0evq+gNihfvWttiuWK4f8VmxWBM/+WK8b8F6Y9evfLf57r9SjuA2URBAobPm/Smni3y3+n1TqgQEACsl5awAI/5AetjNp65A+/38vDAUXaayPL4CMKHYkEFC0DlfIlbAMegyqlmGU2eSTO58TTHX2xLyWvlczc/wY7eDo5WxlYenKyMvNg9Go5MAatqis2Jty2oytLaPupFxOlsgFObsjM05dBxMHVwcMbeFma4xFh8jZxUr2e62Th09I7Bd96I2RI3gzYzqKcsHjqZzGjsamlojTwdmCy9bKFNm7IBcudRU5BU09BQ5eTm5coMaMAw==) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAABMAAA4AAAAAIkQAABKpAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGmQbjEocNgZgAIFkEQwKqTygfguBSAABNgIkA4MMBCAFgnQHIBtLHFWHQtg4AAgt+xD8f52gxWG1uR5EatWEsKGGtrrROAfbhgbsqkcTXk+8cSb2t2LbKz7fybPEC/ukeYa3NyHy/D9ptl4bLoAhSAAYADqGVSx0WQHh8fA07v9/zew9c855UgO/QqKTM9GVxCaWLiSi/R+i08U+4Of29xZE90hzRJVRRI2MqR/4UtI5wcAcNqPDApToUSUYjSpcT+QXXn5a+zaz/t9buUVDpmsnSVyZE7W9V3YRW6gkIqFwHZOEz8yZNyAkBtwZfVEjWAD/BrYL002IehYA///at/ruuWv2EJXQqGQIjZBoM3fW3rxv6/Pmr9n8VURk8MZm0uZNVBEb8CpidRMVQqs0Ks39/d7Xgqlu7zjk2DtDHDX28bUfHg0KCwA3QGEkSBBCijSEPHkIRYoQODgINWoQxx2HOOkUBJ4+hKFzEBe4QyBQwDZgGwRowBZSlGAuvdzKCWRuiw0LAJm7wrz8QeZ+t4ggkIHcd0dYELBBsOACaEAHOg5XQDmgtY9ggGOdJj4KarR21W7Qz/TrvSATe1mvCVRcGIQsiPhIjudoTloJ9TammqzPCWpOKuQ6axSCCp8HA/KFIYINo9VM94B67NppH7YAxm/eIPgij8SuR9/C0+8g3w7F39v8Khj8omzm0JiaZ7l444qvMsAnstouq7pYcvKt26TYqlOZOp/mJ234mjCY7oC4/Q72ir1cq9LY7kUvhugtCr+ZRfcFBtgx2lKDfxZa1hkGB1THTUvPyMzKyc0rKCpWonSZsuUrVqpWq56+kamFtY2tnb2jh5cfistNTLY41vTWc0Tlt1JiorKd6v7UNokwHGZi9R6uH6IMq1ydMgn1rlpfRdJRmagylrRQ9X8wSrX7wf57xx+gdCNMI/I+t4wYHQHKxAGV7JALzIgsitkVtyrpMGVL2oas/Zw1BTOKZpQsK5tVMapqTM200xmXh7ezHie8Lvqe9TvhfxYvsB+ZkbItEy9nU8F+0X5Jt7I9FWtO92/3vM743vO/hxLpkbIrk1DOthIxZQe3B689vg/+D1CBNZl4BWuKtouuAZWi0czWdTk4ZkdOQ2FdrEOKceLJHzd+0wWMrsyKIltHLuRXgyFRKyTrHWXsjlU/FIkacrKon6Kntufn0ETrkHjtUzZx0OTqC6s5ahb0BMBjGGDX48uHpcSXF6uKK0JchdfXpeg0wFjTPqXa6SsWQFiDFb6Luektmdq8Z4N7KWCGjUUnqNY6taI0wwYMwVS4D8YXV8Vobo5NszGGXZSBIBHg1IxjKHIstSPR0KKPlhFHzFwyLuwcF3GBi7rSqWIQgkywQkGgLEkLqWlaJt0CsSUNvS5YEjCWsAQUMwYImNwr842jowi8Y0JM0ECRu8FuAChFDxQ923Z0unuLcwCxjCQA8YcZJC5aBgzsP0q0DIqgBEpsLDHu+aMk8qmWAwvGG0MDtMOyI/ED7w5w6K5Hip6vuNrWFPTiRkxM+Atw56KsgxjkXUCePcgnLgYd7oDlvukRcYy33g9gg0YTz0VG5AUpyNEYAzEa72Oi/hVP1PefFflRGw1BicF4d5pl/fn6M0AiIr/QgnXf9XgDCB4AABE8gAPE94GPX0tAW0dXUMjE1EzY3ELE0krUWsxG3NZOwl5SysHRydnF9cxZ5fMXVM6pqqlrHDt+4uL/Pd3HoagcekDvhbgCTP6+eLs90q6MoH0XWoC+krZxS+EoCYJFlnB3fDNhsjLv3F6rHRznZNCbKlonoDXRTkarIDSk1xxI0hACMNKSaDkhRJiO8/HtVemw6+9IFsLMf/H6jjqkCdNzYE55UXgcEqNlGh71xtqjUT4WUtgMhAUsBp1IQS1Z/FgqgwWjVjmi+W3f/f3MKgU+hVbE2IjswKEiAju0NnCsyMZA2kupofZawvnCLDaexe5ahpUONJt+mt5el9lAKtf24NHBRs6rzUOs99eZy/8b8GgtZY9MltWmGGuqj+p9Fg9n7M5yyy8gvzv8NNEfh0dgdBjGRnFpDJctsFewLwYJITYh7PBN0BrrYwbxY7/h0QnPSolGWtH63Ue/y4Z4EKp+1e/Kt4/e9xUUWRKeRdCiB3lzJEcBdb2ZjENDUI400MCh/mHC5jzQvUVwyqpzwwIoJjIWK31xHDHkUc/VTp2lebQ898VFDAKRlbHESclgpk5H+xb3iviP8hg4P5KLcqj6lG1B1KtVaZGdLcf5Umbu77GiUrmjP5L+yG204DQDTJEXhbzQG07pacEr9XiMQfxkxrYhqKY4rzY11lJf+JFPKTImoiOXyHnnZrg5BR0L3d4MduY6f4S5Ar246Lkw5lRVaT1wuCWp83bSKgdeEHPftgFmimisMyfUZvGLuxp3hlw0i3MTEx03iOW+Ic3EXcoVrwRk8k2qJWNISIsyMjKGMSK7fUxrNZ5lcpxFlebvufLghpowjgyFnLLWmsyDxh/UChbdWgt5G61X1rjeMh5x2yMGsrD48ScfBTnlD6yvOH8rk5YsyosXLxnL7PnxlMo7l4Hy1a9w0eUVuQFmw0navrwA8XHJL1Ot6PaQyD4MlRkRrLHSt/9yWN8BF/hpYvp6lpVr8CjHgFtpvfx47sCIA9uQ6DYk1JjXevTO1RRv0eRL1EHqelsRLT/g5eRbJefedI6L5bbPYyLm1kVzqnMoUbeOqubEM+Rsiuy3UzTtY6a7GqJ2x+yuJZ6rOkak0a2y+3nqY5po5NDaJxkb+kp70Fj05xbbMG8L4hcnpjUqbgqjiZ5bo6PDUH2us5/S/GLntZp13empNkvqa4E9+m6fcRm6h9UEEjanZT+VYOA0rFyaxlzEiIWozs524XDLVyWK9Pl1fl9ah4FaFUOaa7luwJI/mAPtbNDGicZR/xiXDklopOMBv2gyrXdXex9Qr0QP+Z7EOLlnlX/v2716wJK3/vx9/2Zw7lmfQqRY6uv47v/z61fvMWl7dsllN+NoRXRLJa4XXQuISQ/IFgIdFCkaM1tZCVhyftWHsWiwi4cO0hypHbDk9rC5sA6ILo0FAnUNr7eP/Db5zbpWokwtbhUEuMnC3XVr88cFez/J7iFMLc8XHivhuHLyN8amDm7M3b3jrBXu5JGPTxvY5dVPZOvQ3iU/pL+XdwoZ8Xufq89w/+EThnvZeuOtCPoNV9PLt1yoL/6/3os0UoZYUL/B9zSevPLvsRwOjNFRv7lUnC2rzUlLrC3PQnmCeSTHGGA52vLb86HKG+QMEy/globeTcxSvU76nFz+ODv8bhE8x4hTU6IeuaLtoumWzMCpCv1KqRw1aiJ71bdMOCdTffXPXFr2LJvaX+aqmJ8L6XkzpTvxu5Hu+Z3JjMzbM31P781kpN2dhP2fbF26LXxG+Ey+G/gWoHE+jwsIuHqOGOD/SAEXGHBtecGA+xg+Fm55l0f0aReLUfB36cIuJN/PtzMbbwTsFOR9Us0Oe6Kq8jgsC1qH/UcoeMrg+YyB+S6mNaUNYJnQfRxuFwIiPKnNnrQpulJ9pjhRb4jlaIWcZvvt/QdyXuT7UsfJznqArbDiL5ADLVQ+tgR7OmE8S5u2vuGwd0N7NwePjLYynPv9fCvaVC5fl8a/9jwqLk1+KH6c/AaiK+or67Hhup8rP2M1WAqqCsCODTpIjOZ0X54mWzgYaVZlrfyXvWC+YJIzWjVDUYRjUt9qUJCW/aOiKuvH39Ra9JPOJz/RJ5X3C67uhJvddHmJauw8Pvu6o68BTf8M3TaAz3nxon2g+J9F6yCouTOW8zyauM/cwVZ9/Wg7r4qF0EFY5WGTR23ztbPDrbqJAr66DlggpQmUCqI2ktc6vji0/VgJ3a+QzRG8tV056+cVrX4rmJIh+aeKVPO7PFMQ9SyxJlrdz2umkgo6VLwwkm7DSeVJPbDIl64j1L1rXxY4YqVb1OoeItSwZWgYP8ntTHlk39jq1HQvuWAJpMe7OzanHp93K3bFxSkldiaOfN8deRF9aYgC2IaA2KZRgvcN75Rk/4DCTCBoP8vWuZRcWp0QlV4XgCoqcY65FgX0nOz/y7TwPkcmKQu8XT9bgHnsS+pg1ZP0pBNIdRH+qounqU4ApWSUCdMlWxr5eepG7hyNzGfm20202RIYdxlCunYFuWYwLbV6oDf13tRVvtTaYRBWsc5ziwotC7RvLP/7unf4GzmfMqzvKukWa16wenuQ8v1pVqNJlqd/SPI5i5qj7oKFDSxoHSfHXLyfVuNFTTpncMWe76upHa+Jqw1i5P/A4LibI1XdCWekYe3qrXSuJCExV/d6oZDBtRLgvIFnSIku72991A1DFxrtU/2J8RcSXMSt2Sl40JeI199ymJ/esURrjGhvWc/PbRqi1ecUpU8u39xPTU7fX5YalZZdyf2BydhDloC3Gy+vG6yn6g9FxhzmP2TEgM151z3aVuySwHNn9V5JB2yxpoK1tZS2s5Dtih37MuMoXx328qaPNW4RMsvhpDTd/5JumdXeztPWSSVFL5De8tqQ7AoWPaLUoY2qn57PHVMtgmM2o46sJW5F/Z5+lK9eSXBu7WAhLlI+sfhKNfKamhssA6acpIosveN6+n5+EUjJJTWS6kvNQBpj8+aQn+EP6O/P87Z1hRLpKNSqkK3h/+gMTznkPUgp7OwayZlPisz+WA+SYzYtq2PPnwQlJQbfKJt6JobRdU+SdhOyvWwn4n7HXNvNaYXRRNFYwZljS+MbfFAoifo5kQqmz0hCffns7BmxmzMpGVP0yv9MSeTBp5R00DvBIf+qeuJmetWnoYc1I+lpVUOgnV8XXpzkp0gvn2CpQbgWkQe5+eeLUoGrAJ+iNpBQ/+MlZjVSrCtkn5cWdKY6++aRiWLwZ/vXZfVf9+Jprrt43qhJpz969Jx6m3/YL+1qaOJCRsK3wkNxOQzXSONrr3rurtk6zL26j4kGDqDWjX96n7eT+hSzFivQGbnFixZSoefqaxz4y485zrlK+Yx03F4m8TWAkBE+TYBmdyh0iRAQ8vAOrkkdakPq/Qmhi8M0u2kCXcmHPJyjqs37TjtyEbUx0c2jqpyiyZtgmhf+0oHuDvKeutM/9PXrR9NGxC47vexqREJuyZ1PIkz8kzWvKEXVDd1PL1NNOfztk0jNacK+mJ78gm6QMKRZ+KngTnB1NcNLFvXJmkjayKXi27Rkk2VsDGX7JAs1Tc8QHOUvgNszUqrugx72JvUHBw67Drv795tVuNp0GyJKL7IBQo+uN+81tuhD3xu6vHTGL+QOQqJtokVIIXcILpcXgUnK/LFrW4HDX3TT5beTB1r/GaIETDHKldelz0df1E4ihfLpdfNpsN1NNHvpb/gsMZB/CQcw8YB+CgyN8yUADVvYm2FSNC2Ph4qm65UMkci0r3epgES22xM3L/qlEKluhrjZ+UuhtjtNV00kwiINsiMt0iE9MiAjMiEzsiAbY81y6HBVyBmoUWy9dbYTKD2Yr0XWr2h5rlg/oxWlCQI4NnPOWI3yuJbLf9Q58iIHcjPOrLZuXI9sE8MD1GCYo6H/uJorUZ++UzRZd6xl4Ii1s+Ae/gS82P1bbJgTAuPg1C15kJdLdvKYYzkvKm3QHph6tVrbmOBiOAwb8Mfc5Y/6oxlh03uQ1fufCXA5uPge1uPHcvgr0B7wDdpxXofNGVXbg358YQOfgBq8KlgZ3ofT7Nu4Gq/uNy5o62c8f/GsrYyeeB61HdvztNxNt9jXF+2qo245pWWT83VGKGurvyDxznOvPJY2vTevxG69OIj3OKdWuFvQaNClgedPvN5rSot7RCb/lIAA/fgek3NTiS5Wrf/p+JcA+OKvoAzAL83hv5/zn/GV6jIcWEEBNLC4f5MJYHUVFPfXgj5XXY13W2TwtHBbA+NMQilHrc8M9eP5KB3n1cDkz9/6LCNe1GDCVC+1utfTOYo1v+SSOc7HAvE4wytTlXUe+RkelmT2KhmFdt5wZg2jjugI5TN0qGeumPHCU7q7xqOJ9UhzbjgIzSSe2aImUZQz1ZW045HSAjNVbmaJ68W6Moh0bPPKbvJBWGvUcrVK7POi7FHLdZS5PIvFJUlsGtTUNGMx5tfIKPnxvE52XGmPglod6sU1vGujF1f5HGi8dZoFMc1DQ3NrXKMRyDd5I7/kieZBc6L5GLOyvpFHEmqF6iTJ732AALfJxsMJFgKwA3SoE2ggwJI3NCRXwI1AG45gcmk4CgvCxuiwMYaGY8mIGU4Ti1CVVxZOFMPgkNgwPx/fCDF1VbVssJhpsMY8wGt08yAPZaFfgYCgQ7MMV5VXeK7CopLyVK6oYHeGCIKUT2S7cAOlC67C/UgG9QblFo2Tmk7cJ202gUvUXU9OCF4lw2ihDIiQXHhAwktVwWGNoCL8amGvIJ8inPdkZW5obOMoJM5HlSraakb/CJ4AAA==) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAChwAA4AAAAATeAAACgaAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGoFOG5JCHDYGYACCWBEMCvI82x4Lg1oAATYCJAOHMAQgBYMAByAbcT9FB2LYOAAQlrxDFMHGgYhg7wv+LxPMMdTZwdcAokVZdtu6RLW2UUDAMvAbzZ4j0u2S99aGde5X9nYZLo8RBVE8cz/ziI9IIx2hsU9yf6C5/bvdgpElUiKlIGkMA6ENkDRIGSmVI0aPDP0gFj1qoiBp0GVi0dYXJuYUHnju5981VVmCjIc7w3k0B1KTz2Y/Cgf0o2mPp/+Wsb87U/V613FQAqHQIQuFClkirPwW+afv362q6gMtVf/DsOf2cg0vvM3O4NPdzA4j3mvSUAnMZjCdnkUeRGKpRucwnAmqcD3gCWVZxcs/tQMPwPr2Toq7D0ZhBA+fWm5pLolxQRiTsrNzhdLu/v/ZTNsd76xPmzX9ECsMPVdARctFOfu1b6TZ0Qr2zs9a7YHAJCkso86kM+kMVIWLhlmS7ehCzFWK3kWXdCna1C1wmaJt0sbWSrOImtKwHO4R5x9/Su4Fx+oN7ec3pBJ8N1JXHSbD5btBxdL64RmbEBAY3Hq/9fdh7HIECcLYaYizzkJYsIKwYQtlxx7CBRnCjRvEFd4QAYIhwoRDRIqGiBMHkSgFIlMWRJ48iAIFEFddhfhPKUSZMoibbkJUqoaga4RgeAPx3nuIFasQ6z5CIDAAOAEIw0DYuAAAoZeanZz9sN0XZ6xB/jMlyAfkvwe5eYP8n8shfiAPWX0N8gNeCG6CIFtiqJtf9GvxXgISaYUFoBbxXMhQubGvc726uLHg5rjExJR0Tx3ZrOKw5Wn/QhIIl5GeLXqGlHXOU+EEm1DHutZHMAYTy4QF+DDhMBH8epbUgFiWLMcX9MywrBWln49cqDPvQ4V3wayqvCnfluUTUl0J7HbL755hb8JZNZvW55+vesv6HJ231QTzFndzWbOdc8i2zl2YaW7Qf5NqnzZydd7kCi/4mZFannpkiTG74hVPfJrDMXEFG0XiGV61ZftA1KS6oDHeeAP3jKIKTrQnWVM/au+s0gpuLGx6JGRpNknnE/R87HG7/X3q08E1N5tZM1rsYm4z4/l9NPux8A3c1CCHpdjQ7GTZ6Lb13GlycjkCAkpX5OMRbE4ySW9DY+dXaipDaJs3ojPG4jQ/aul0PNNO51SvCq6551maBRVcYsmllFGX/glWV19TjO7W3L3u11JrD3rUY4OGjJkwacq0GbPmvPDaG8tWrCEgeZ6Fl3mRjOJz+b4qtOU62xDRPocXYTmKlaIsl2epAu8rtRw7L/FFcIsiuSjuRVssxZY8dyswUqnarhsKj2STBSYvm/IxFWK6bhORl6dRzBZloWj9pVgrLy4FcbpuoTJbEKXehkPylYVNXj6Wb9t1n8Lw8kmoR3TWRE4W8wgJf3vfKTaK9qJs3V3zptL4Qpy1mTyS2OS5Z8GxKIkvxOTlXpzcKkQXpWTHE/MpxWrZvMuXX6GGromqNB7X5SGirfclgrSaKMJaUd6UZ7oCYbzulpx2Vfj0rZF6IkS4yRViSjiVE/o2lcf6/ifqxImwExxRu+P52JE0d9ZMFobyQsa5E8tBMibGQEbJ/86R+2jx8unUVlZtz6lB4/101XTo1O3hfeW83xYwNOkYEHAcMEwBdQr4nQYiJyBwAS5k4OEK7NyBnSewCwIuwcAjBRAZwCcTuGQBjyrgVw1E9cCtAXg1AocmILoLXJqBx33AaAG8VsB4AHgdgNMp2cYr2CoT4PIYeAwCYghQY4CaAIJJEDYFRNMgbAaIZkHYHBC9AE6vQcgb4PMesJZB0AoIWZPsJRtbDaN3CDgTY2BxI3zm40jcJ2+Agh52HAmVLY5u0AJ1mAYevFW9Hk5cWVXWGnpmBBLiEKpMwhTCt8CtbQ8RAdLHwZ9a7CAeIc2s4OtgYDG2Pjpxwqk1ijOjkDHF0R8pTV6VVGVVWSnLGhvATnDnaPTa7RscwG2qCZBqXEJvuR+HcK9aeg4AjD+aG4NunCsw8A/AfZUcIA05AgBsu4wM0lAHMzYpiIoxYEMGQpb77cLCRF3iH0poycnN1KYpHZnI07zLdhEcbwX2DsAuQk5AIpOa/NwKPc3pzGSe5X2+F4Pj2zvgzzPwZwYA/BkCfx6DP8vgzzvwJwsAQhaAHAAtegAuAXABQANQDIAO4AiSZRUqmVQTrBfltWpcdOk3unyJA0dOv7a+s8u15o7o6rhy487DmvX64r/wssZM/16UaG+9qzZPLQZVrDjxEiRK8sqiZDQpunXVnvIneqRKo5Ofeia9dv1wN3yQ7bmPbrgJgcEGEwR4AAB8AgDIC4AFwF0EQp8Azk0kx9snDfPj2QmX1DwUzSr3I4rZnsxV4KazY0KQuDQbrywA7HwxcI2zw1xZJWHD5VmoyqDaKJyscpqjkz68f7LUJy6TZMjXsyGBTFpTFyxonNXoVAXBK+0RqSefAlovCIp7zRt82uqT0UeNC68eabzREGvrdZ4TXocmmhWkYD1RsgYezAYhPBKxSIn4L5uSmEH33PYFeM6NZWmoZWzp0TlTuLIqS+esrdvL7Nr7to4j9KKuj2+9hmHQ2OKiv3OXFts0bnPXvEqCGte/dZxZlK2+x2IMVoKF7B+O5qvBIc79qe2ZIEetij/Rwrm+btakPVN9/M1ilf/npsR0YlRrBCW4YSK+CmBFQujrC3m+S8Ju4LHpH4nkYnJysgUVZxSJlOEfwx0uD7/GUZVIIPF5RdEjGmu8ReZm/0Af7uv5obkxNwuXvMKEb9rW1YbViRmrKxkPVLHPjRCrUuB8wyfx31SJC6Nswq2GEtXJdqucBTyVVflWFI9zuqybkrG4M4ci584piF0xKvC7dDZutTg/3uCJCYrLhUseQJkfkHC2z5f4odJxAoxLNLxC90Y6jrVmk8BeFvnl7t3h02X1SWGkYoNSa9v6o4H4GMjKTE/0XLrT4JTxJ63l9bQdeBsVy3Qi6aWJAGq/sGaSew6pnQIp0OzUgzA0ZmkKQKmtrRNiMBEVtmfeMNGBreSPDRm+vvA2zXhCBe2aS5P7KP6IJJSe6LBqz5Ei56TaOnWHeMhXMl445QWnFZOTK803ANrivZFmoBgL63JZ9voy6IknS+56R+f1DWvsvzpzWB19DIVc8mhfy6E5YI9dnpv9XEuRKw5QatQBLigNO8rTPRAhL1ec03hBwiMZFPTqL6H1E8/2X26SPWgBVUSts8n7TTMBJnmS17rjY3dML++JaWooj3xhV5mDb/e6xR3zRy5FfTvPH36NYQnfQbWiBzQOhBQ5NNFlU3ZY8czbQpnpgWi8Bxd3AwmPyNunMbt7pGj8G3WPuemhnnQlaZ/XfHpFTPbEoXsrmVvI0fu0cbgtWw41hmEIFPMty575POf9RhrpscIm4jKmFha8ldjdERqNKyPqlpb5Yx5lYIPBpkfcNt06HruzrseKVty0SzgorGALbNwvz73l6DSgh9lhy2KT0YjMaVMpauc79mWKtENlDTy3TB2zK78JVdAuz2w0NxmcWeZ0qlUa9vL2OCOdWSGZlmkf3HPSIYY7a0S3/otI0hwP2NMc3nI11Yw9k91we3kEECrWpHCdgDlKgVPNtLWLhKGF7ZcohA1gH5q3RQuqQ9w7NZqlbv+7Q/1JSsRXVky4J1YD2CPfs4lhm3aRb+QksBZc9Vpr2pq+7e74y7VGwdNegL6iDqZspLMjt1Jnr8RJxqWejmg8fkGF2cv10t+bZuJfdfXPvbXIcnSO+jdgneHNNkGGrihbmX3tuFWAEnFZT8yqnElEyFDQS3jJ53msXUKaLu4COb31KjLUCrih9oZ+oCV2U1jMFR+7uoOwQr9Bt92PkKHU0+XtBzRHBaRjrQ8Ozo1y3CQFhrEGQiXh6c+Yk3OS0PGjp1kWoJsDDYDyY76UIooOLWxMbUjT5MpGtDmhdDPZeE/yZN6kAJsENoaioZ5z9T6yMnd4KpCjOCpsYhmKimZZ+fN/YMfwcGHb1NT++2n6XSxcXVa/7cv+z7yc67dNKC1uT3ly6Y4N2FzcuokbcsdWvL64c91urT0+S6b5Y9NoJtq1FUS2QwazKM5dkkAXKnwc2dalH0j3pZVp7m0ibj1VOxm7aGk9cUJ1swGfbRL3K1/xsqijM9l37rdPcj1YUsMhGj22xTLFtjLevfZzfUhAaH1sl06a5+KxUWpZ5NA6lwq5AYkMHJNyzWTEcMzt9QSBF4I/CnlM8mQnAD0w0wsUUvbYpS5zi9z53h46FDv09lxT+YJVojc2chBiJIEjP9H1EnHf9yVWXllTdsCXgLOYk7njJJRI7JaqdR+PaAxBj4Ixj3iVnFNCGAC5ZsgD8e2siOrkW3FY9TOPfWXUmyzb8TLyQhRynZg28M31dCzs9s3yYP161d7Nj6uDvmW1UuX/42VRsAIlj+oMsGJZnUf7cGq0+lWhln14YqScT09o6NNdhLFMLPs6Rt/oMIJoYsJ+05ZQ0851tewu+ahpupMSENXDo1YamhshBb24benKkLp/2j7Bhwb5F8LHMN5mGnOeJedx7kuL1Sk58BTb1HRQH8Xjjccj/qw26c1yh6jVaDNjR3aTh/qjFmumg2K/pX94qWuvDJo1ip02Q2eQ02g6RRnbLeCtwrRLt2ZpjZJWHntwl3JkNfTJtiRwpF2S2XLbrM26mbBffNrpp+pyqeXm21xNN9Lt9yvk83Yn4ZYadaZZaBh5yyzmagub0aLuwO0yDo5dK/mrhwGp878QcWE8cXe0tM5dntMa6UQkrkSHFYGqUlwYKhXuHOL24SIK3ADReAvoQTmilsrUuhnkg3XH9oLaiObS8RGrr9mvNYY7Ww4Zegzpa24s529xTe+Qx1uq9GD2CEH4GR3bxE15VZk5T4U1CO8QjVBO8RXNKNgUNy6YLDxnJxCQCAWZYem0Lu+Z7QMtFGGZPvsoB8V9FtqJWcSe87O7a6ap2WYfFcU+wDH6UDd7wBH4EgzD/ucIX7qNIg6piAMKN4wTzh65pEwDw+6X0AhennNwVN1KK9SSIOvGWJINZbCRJatm7MDs7guh9X3YX41sFTkHMEOpE3lHeGvvbe7FiXxh8V3PT8+uZHxF1uM/1fwoLypKFiiF40Hpto87R9oAx7g7dj/fFizigJWSkfIXcIy/jhmOLLjJAhyDBbv7GeIG9uJa9sanxm9F48WXXVrE5y6Lxr1N+X8ZsHjfvFCgx19/765gffEJmLKcLzbkr3flpxfpwhwLu9WK1FS0AfLB+msHrqrm/s53p7HLA8t/lnvGEkGx4I46l9yD6SeLCoeFjgjJ9yy2TcuB31+zu6KSiddE/4lKFlwTA/Qfh2FwRE35eHtaA7T9X2Rs7eDqbOVlqcu8GFoycj7m4buHmPr1fEVbPkyjCdXw91hiSoqDrZG9JRxusAv3Qs+uoK6hjcNuoUvEvajYD4Li8pOtt7jWFdQ+LNw+LJYODQoMaj2Yyf1eU+2t9wpXZgIeXnH4+yS2PvygvrVZSW0LLTJImtCLLwqL7YALAmuSsluSd6L/vcvKWPwqhnHpZU++Xhpe7UlLiNZ1fnaFXf+ma2QGb/QkP4ESGA3CvX1haa2XsOm9zI4AZ3vHfON4HBPwwAQz+Zsx/5ZSC1/yirGvs92K/LOcVrzCr/Zvi606ret76qP2isxHlPCMLoD5cTL3KUEbOc6ngQuB3DZypoKc8N3u5SIqvvzahfez9mbXjL29nriZrL1InzYecPO2Gnr6Yfr6rvr6YXr6Q2rCf1dBq5Kz6UYThAZAArfV9wdWslrajLf9NN6rcv0SAsNXLdQ9KOIpYOYs+Dfjlu6ZeSsaY7Dp+o3PdRuPjO0c3S/YBV3Q2+TPZ7X1v/FLSqANInOfMR/THrClXy2jpV058sSk0vDQ1ImDcW2kFNLIdJ8HEu5odNLeTKN5jUxN46H2SQb6UCCBSWKCNNZ8WWDfd6mSyN/PM5Nh/gt8TqWzp2TfCrdNlz+rZVZmeGxajyhwyzY8iz+4Rcw/gAIHWlapTaXyTaXUVr1TJkmmJnogn7zz5aHSn6OysajSDlKFy1PKRLwMsfcb8TfohyzfWmYBjnEdtHr0E4Rzuqs3//7GbAurbYuGsUL/FxY5gH7bYf2D69lPYkV8WMBF+vjvj4gg7yhzSkSQ4w84qdt7Ui9L2e5xjjAp/lEx8+jf/bytoxSzi46BZ04cdTrlNdgwPY0pOBFt6+4Sf0FvqxRtH50n3AVtOVJivnjVeAX2nb/Al4j3AlhJbU6xCeYUuptdA4ifmeuOEjoJYL4VUh7CCqG7BuvstiK01GjYOZU5s5yLLzip363aLUAkwcG+PS4FwbG+eUF2rPDE9g33rN+Cz/vI4ZXeByhKcfTYvn2rv0t++kZ3R7EcS+MiaHdi3KKy/dLrhu5wwkkcQ6/zXArfuH4EueHcPOONYy0/FNPgJrjIdibf0B0JsiU4eqktEKd2DcHN1j0/xaTut6lcIt9964FDBoOP+eyz04yUkpMTBLOVUp6nY7cVGTiOFVibYE1Bekzo1cZypWoQnU1UvvXZN2o4eUzwxxdEpdmf059flOKy04P9MmKjEPB4JlBWnFxwnb6EW8CMYQhPGUu3Mgsz+MpYIp/lCFv3eKrzD8FY1GT2YY5qxs99WKE10JoNWwjbIg2BvsW9+HvMe3E/m5XdNazwSt9qgmqZtcHbNUqWqKe2Kuig/Ca2EWZ72nU7ijYZo9GjloHXvLb0Qi9cuuhpqW9uZ+jc2HT/DpKk52Bqec7X7OhWzv+t7cNvykEDS9oibc1UT3/91QRWXVQ9k8RkeCs37afhqjWPwkkDEokZpiEQwc9D/8Q4DcOC5uwm9cRlgXH4pyyI8qiRmGNKo5XKk1NMkgbwMVsqW5gkZm9lLxOOoRQnCpNi96QB3jK9HIQ8X2/MDZ5hngnzvOzjQhbmZEL8uy/J/XbulX7VH4d7YYnE3OXw+aL7hQpXRxsAaYEMm1BP8xXX4MZhj6BX7CossdKIPy9T8qIG3X3bQ1ccQsNs3WOucaRa11hxJcZkg48QA1n4+XlmxacioGJjcuvLPPIXG+oe7+gVGBeOItgQnwTyZV8qBQXHOVIzPH7+snvQKcsta7Rt7lVvE7MpyMrbyMrNO6jpW1OQnbf5qUuj7yMoa5FkD/3oxSyPNzYszzxCv5Aa6xo1mZqyMhXUz3aurhdtXDxtERDTN29h7y6SYCupcz7Nb9NfsY9u9H5A3lZv3jnfGUtofT/2Zz3hVr4mZvh+pqv54kUElAksov9mnnx7h7Ys451CQ+xeiolF10UR06Kz/C6Ge+DMlzFu4U3D5JBZzF+BlzcGmCQmHFanU+nv6MHZtXhpN8a2NI6Bl/Kwqv4BS8IOIr0idh7CP8QLSWvi90k/ynt/knGiZFEyVLt78t8zzZXIqv0NvKcH5a/S99a1qKn8HhOrmp+Q0/vvR2gJca8yZ/QR7hBhkpifQndfAONyxb/o12fYp8EsHyQu1C/H85IFy56aE+KLiQlg+WDe/nrBE5myHBi6XjMNCc3IeN/0KKfgi29CL/t5u2eQgXvMu0B1CAxEDmBub1WoUJx8MVEdSZ6FMsrQ73yb5HrZndrlS1aLSFqJSqkzYGL1gsXmBQVgovylE4+s185AEQMKtMimNUwS83mlwLNvQi/7eLtnkf57W/UdfRCi+huk5CrjmOQVuWtQ6DP7REtA9B3ffRy2//rZ1ta1KRiy91Vdi2uJCrdbESqNkV6OnAiE1Gg3pnraYBovUf9mfskku5DwVUER4gQE/z0aZOQl0S7y6kdFlrlzmO2eZyfri7cbpw7GoC7eObrncuMPFLUg/jE1tFug7RNmfqKQkFdb9J4d5c8rmeIQFioWFGYfB4sgRrFqBl/tNR3MmMN8kb5A4+r5svtyq+V/wrMuwot7n9mxB282LxMXu4jPHmyAmfztaNZSauELflH2DWf6Pl5NK1oSUEG++3gn5fGkIjwpiflXXl1JKuSJB574pEJwThcPFPdb+q5VV1oc+RhZELVC5KOEk3y+Se1lcMF7XwFnAWdK90WZSX034Uct0rKVw7zlkrPCy6Q/VO+FPGfIuix1gLomyxuEkbCR46OMH13gQNCGLCdFgYWbiP8WLus8cDlCNunb5JnBRFaknCpOjy52exLM5F+82tsl6dfm+1DylcIi38vX8g8lvNt8Oi7vj72L5hcsdl+8fzXh4l1zSec2ZzPp83eLEm0azKQ928DckDGx+QteCS9+/T21FFgWWLY08f82Oie9uMWaHHNyy4oTiHPLclL3a0nYToGggFhP6bv0PU3GKk324alfgp6evDTZVx/3GnIPmfmJLUToWuzzrPVQdwpvBP0K446XyzD6c2x2taXfOdclt6d55g3ah46/XO3sNb0UEr0dbRmif87BH7xGPo2A1yBtoWeVyFbu1LRrlSZnlSb7+HSbkKcnb0pdJ9J31l98MnIeWanvqqMBa5E2QLkU2xJrsCoOqrGiDqORZoUfpebJkD/uM1I7Rr/4mjJFoKQcJNk2WPJ7Mmtedwm0Nj/faXAT5sKYV5qlZmRfSZRG/HmRmh/d7+7XEbZiF0y5EBjfVbPrdkyHP3INLj2WrjOOla29f7zpbZY03ShWjj7sIUM3iZeltxnWLxXK0U9TpWpBtUiaygD4LAveDHgFosJCX17JpvJ6Xjm4OywdlGgKESASBoo2r5K6oYjkb6EP0kXCFvokfyjqTgLVb0zrII+HwR7WAaryaqpyaouC1sEeDk4h7jaB6vqq++XUjL/bhLg7OGVkByV7eVUt/MUSJ1RVZDnGroqYpPZpi5NVZS9YZotbXpei0gqadBools6GzmjFnW6KxWClThJfRs9EuVw0MmHorFocedIodeKavr7coNpsEG9eMwYGeweVl5ACQ12DfuWD6G6kwOCkUa8yKGvjZDG+wwMcrl5WM7NZln9PwD6dK7Gbn3ygVb5J/p1+EhJGofmQU4oiDtJ/6t0/FZaTGYMcYqmZFwXF+pJBH8P/zbfYi+Ln4hF+QTug+UoIwgTci7dE3yvxbQNv5fGbuDtx3RFFupFvT8YUG/F6RfqSL7jLnA8FH+LtGlkdDUFOohIT2hNTmnuQSGu2Lgo/fJzksPkVU0QKt+js8ISeGSRh3bBoOhdfUpxtNsAkDTGnO0isEJ/lOLHf5+RG+cZFX0b1iXW/+K/83yFxNzA1IOkgNoe0n9YdaC5tPl+/RdpinB8sHVSYaAIdl4CGANan533zrhn15IPMNsnvaqCF1EfVb4UV96UyfJSaVFLw1Ro6ICZgmeHo0ev9ORabHgLCKnvP9TmEhRYXABb6J2N6U8oLZy3HM92BKKB7pzCGsA/7+rL9Q3rW659MfYiCZ7ZHQkVxSewIM6wqjEnKBIcAoTfNRgVGDzr3NdRoYx4ON0Xvfnsrc8495m1329MX+GZ12rsRg9Gvn7TaerZ08QPyHcN2AlcCRZNc51yMb2cT5xud6BesHRpvw5lc/o58bcrh3JV9J7F6ky846CPMUwVRplX/jcaczC58H9nZslFY3PVvPHw2ruAM74XNbHq4t4tLbZT3UZq6Bin8CojOfXLue9h3WTZ+lbXMEFBeczoAfPfCt3t7e1+2VEUwIwoEMIsnVUFknjGHXDU7bOSL3Vcu500ki1YP1fN91EnEn/ixfGUb92sDXo/DNtPLgAubXp7Rwt89CYxzW+egLl6So5yvsoGTCUl5Gx6/qdiMJ64iy5N/J0NYUvzjWwXHHouo2ljtO1oiUjVLb2nNVGos2EW4WQZsMmTjJE/tkZGF7rt1hmp9egpPVaTu+fhItf33qDC76RU8FZgT+y0wJRMvkfy4oLbI44BkH36rMzbcqMadljj6+ZX8oqiw1wglAwoD2AI78obYB96101gMXZfcUfzFxbP/Gzwh+iMUCxwbjDk3Kna+b3B2aK9NCdplXf/GCBkOy0xKZ2tcaI/TRrdJBcRCGTGxMX8Bt/6gu7/WkME1oHM8quNarBcUORARJLHR24uC5vbHVYa53A99dKIfry2pnw1QEOrT9Qk+5f3k5jEJRg3I6TmZpk1h37z+f6y6WFNDrb++0pS/CFvc/Zyva1qqvf0hHPi27DeWB3cojEGR5xs9/eJrHzLeucc8TGQ50WI9KTlU18JrSXmZ9XBAP8ytLxNKwrtGRBfWH/UIbXxMW/KIfBjPdE5N8oksiPUq/i+hIKcODpNLhYbi512+7HNw7GzqmOCfDxjNKbxSdF5qaEh6bgQGgj7tZs1OCP76gNESYq2edkC807DRiKn0M4nT25IOe0cRA3R2688oxmwYrxyTkxYSmpVHAXDgYl/S7i13Dddj3kXMznrqByPxrWgN2n1i7pPwBdVWTAJSHf3zXVImoNatV5pH299g2Rcbzhl5JAZTH4/foNSGZRkE4vRh5fJ4dT4k+oROc9mNu/4C3MzY6j/y9nEscpZNx0TTFQlsQe9U/p/Rtthl5WHEHamh/HielF6F3q0i1B73i4rxADXej8h5s4uIUzaGihbp1nzanywSy4aOrm92lWFuBhASTGLvrCJdPW1oYvHoDq5HcARZqjzYZNp2AFcHxXbQM5ELcUH+H4WEMT2qXzCYl8NvltzeG2GItPF6MvnpxVMJZw4fCiOYlDMwjKTAmKQQaC6B5ncz2aeuWJKl0MfSS+Fkrwv5N+rNGDpIj1xnvZvHc2ujhDP2h2JwZlUNkGBd1Qu6IUs3RaS4iM7729JKkVMjQRQ2j9fcu3a9zjawPE0+4Ue9h1ahHbpPv+9yUxxA3JAq6u83iZm9/Y+7QT04hMjvxitczazHWCHx0Rvwbh4szpENL7jfRK+h908MfhIyP8DARCEl/isDUTE9A93QBucqGQa2Z5yO+yMxzWhlTXyWmkd9f0fL7kB7HrH17FCX9IvGiqHGgPrtDkYHk8TsZnQzZxELCzcjB4RciclFG0+MfxSzV36IODf0JaaGEvgToUOwXrC0RASp52n6T0K4rOFNyoXjD5L175T1rXZBa+/6jWgkIQkTjCnUGt2WZ/Cfh/NIetzYhi9cbDyHGOghRuH87h8lMhAL9OZ0U8vabrWfklejfr1Lz+90OqnS5XIkPSi9q0K6pOAhSGot9YzHjfdQrPtl/h+4Tm6LQ8FY0Fmb5wVEC8INezN6rXitLciGDohLIiYYzT9R9nFflGgMHh39utkT1okPBPWqW2vMf7SGOEdWQmY3xvMWl+56318u21C1+EqXftUXxKu/PNPbw/9evBMSnVsbRH6u2Tr0qOyOP2jMpJTRy0DPvz5gANOuGXXeh0itYTM35i4mZI0Rh/wvXzIrMgrg6tc5Ft2MA/k547d9f+C/pfFj+uNHfx+9fXM4ip832R9/5o3vN1k36+h1HtfHbpV+B+oU2/TWdDm9/NFQ38IfNrAl+W1OjNHHBlmD8/R5JtUnvf3M//lW5xp9rXSrtI/eJ+XFXSbh/CX7lDgcay5KKSz8r/BWigrj6cExAXLqXGZlctEBFNAOfFq0d+EfsudKbiGdnsDbxjlMHidz87VlAsiDAgAowG5EAjkOBMBi43YGxC5VC8LVHSYDTSF72TR4B98KQFUNnBu9bWDVqLqBBlM2A5tJtQyUpnGps1TIwDyjygbWkR40UBuiiNgqNapBBppK2QxsBtUy0GTKbuDmqKaBXXalLQPcqlBapxzRDqjYlCvArZ0ykckejp0LfoNytNdMgBmEIaBoYP2oRgCNyGPwIBMROUaopwpSWFOEW+jpLdGVnfdUwaAwNhuAcrTjaPmqfPAOkr9zyzlAcGTntoaHhZ0KjZec8vHAjSBlI0LkZd3Nbsxu5BiGzXpSdphKitsIviMHKc+yEKfZQAS+5PAgEuEixbxUcUowoJPwK3g7JDgpNl4PwhNSJaISZqO8EMgji2CEQASJ5XOxrQiUI6fNsG4GqkJQFFaQk1JNsY6o0w/LyLKlagbkUI52BDcmR1DjxkOjmqimjokeBBCSNCUQCQZtv7eEnEH0sGLQRUcJTL1NhXV+LFXSYZrTBiJ6sIEkcsCcbgS3AKLK2QbCQw+O8GBCYB/HyQorBMRou3LDnttx7iHJ9XbFWIaUWeVzOJ87eVak2sZtlSobxyQ9aNwGNGmVQFUMn2jURsfnXUuje922d73Cg8CcLrdHb2Wiz9U0kRvPoemdRYvLEwCFF7WLSw6tb5HlPid8ldxxOAbJfgdzPySlycbOlRw9PaSQvCQ0Mk+UiCyRIgokmzQQp/KK6FC5qHlBmYuaFfQV60CKvpf1pa7k6HMyqHWdThqL+6bnHZ91TtcCTsdGqAhhKTJ68UEDgJsEzS/ZUhXeFtivYe1NgK10irns4O4aM+736WHfPqYXKbHtdfbSOfty1ofj+ch4OH5uC4Kc/qkM0pfTfARJuY4c70kYELZrD0mAn/T5UuFfJa6zJFzan84/XSUNM2Jsf98BoV8Gkx1MUs4p3AG2t/awSoYjtmeL/bGS89LFzp8xj0d23Fcj1nvEdH9O7BJxlkv3dcxupbgk/iMawOZ6Wx5CIJqxPbrvT5VcGDDXc0w4YV2R9g2J2aiF1yneO8jmEmWRPNdxZ0f2xyzOR5zXt+dCGxdDF1EbU49O/b07sgH2Fa2dAHrpI6UAP1jskAMdd0a/W0fxACpXSRhl2NN3nFP3zZB80c+3ojSRQyRZnMW7X/jSb1f79uhllIyYoQD0fwCc96dwYs9CAGCaT8+yPv3NeI7+YxO7AwBA3zvfMwCA+ZDlf7/l/p9/2N+DARBhAAAggLC+OAGIKypwncREdW9XnyKZXD1G5AqQE4la4e8R7qEpbJPCQ0/5QmaC5t23l1TKSylvEaLWLkWNeZLs1KdZJRAl2WLjP0CfSZyRZA7nS6UreX+fJ0wOcTk56uIZLfSUYgpYnNhQpaUzCDdIx5lzh5mvO4SzwLQ1CltLpexwpGmyS4DcnuN9XpI8YSQj7GyuocVPTkrIDNo3v4p2btsTd07x9L3vFstU6pgLiMd+uxRdGwRo5QSJy/PLntBTPweVzWdxXZXw0FC+fsmJNMXzK81Gckoq84rjReXyDMtQ6hgI8TC5+u45xT47fAHL3SrB+t8opVL/LVd5dpQVdhcazmOogMLQRGdLaaRR7xKEZ5Zkx+b37bec7pebOtlTRKsVjo3iDoUruaZ6QY99loyVzjbqKPPIjss9QilGpJY6lQaQ72/ZecWpIeISLKQ0SSNHOL17tDJyEyF7FKl0N5k2KU0q6mgrrDjaoiqcCDlNZZEqdvb0DhmkdTbh/e5BKSGkSgDL2eQ5ixzHytEqOpAoJjkuZD2kN2V011+Fc0N4seCQ/WxKJ9PdDGojfkyp9DiZs11uFZXe7rE/eDejhQSiYI17g52PezDzhzd3LHDeEU9EDzHEeUFEERvEAkWIMOLJvzmCiDSiin1DFPGdF+dNIHaIFf9G7BFrPvd8iygiXogn4t7nNyKLGFbML6XjL0dPUH8QT54F8Uec+dygDuVK2Ll5Z0xgf22w3/foXorBbtQ71C3UkzuAAPgkhzAzOKEETlaCacHf74qNOxQSJQKAI4ClbRHiHLfF4BZRi6ZrsbQtjjyawEOrf6zcrA3Q5y8ARRAvHjyFkKZBjboJSjPmzwA+3HZsyg+ZqjjpEJ+4ZbYMFoVbX3ATJKx4rlQdz5/Lk4T40s4mS15C+eYIj4nn43KM2AaDBPOSfiBE9VRNh+hg9T9kun8VZFYLAUgOGDW8oOqygCrI1J7dqPIXxEP4REtkbvyQRfCz3hmm9BkyY9VJFYi8GlTvmHaWXAE=) + format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAAMAAA4AAAAABWwAAAKuAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGiYbIBw2BmAANBEMCoIYgXkLEAABNgIkAxwEIAWDAAcgG0oEAB6D426JQgSiDJGrY+EepR5ejwf4/fWd+/C1EBKYZDS7sRFxHTf9uCJn/m9Of4qsOwRQBbqEex0QSbKziM9Pj42dA85/tYTLU84Cj+f+PIAlq3AtV5GCrQWUqr11TNFedSEUjKs7rSju46fX7RWCSHFAeYQcQRBEKIqiAgIKlGZBdO5a3w4akEBWj6orkgSzThrq5iF0WjfiKGe7e/0dAHkwOR8nW+GblHR72hyEGmzEl02NcDPu9oBKt35NVVBcoyEuIJNhau72SE3EHkhapkdqCiZGhBhliQWUJVETSCQCNfr8o/boWoBjI3miLHqQC4ojH22AaUBxFAUpIBJlJeIVGIvLFI6PlFi4hGYVs0brZ4ZZlT0rbz1SLT+50xlW3X269vh2x+CpO/n7bw02ebvIys0wMkpteMHUIq4PGfxCRBdKjxXGaDRIc42rK+a/qgeebsfBvjGMiQ14cnJjW8fSe6fHlr2NIrgbeH2jS+k9X+md9WJP/5IvZ8LRg1cQ3gz+dJMePnr2/6ZSiy3c9rHc87Zj4tqOx0WLe1U0VR2OOEt9kq4gV/r/NBEyVbPvpL70poCoTunu3LVVZ4nW3xWV8gAKP5VqBMD10Pruq+7/52x5c4B8EQjkzs5oyJ/1JzxT0mgEACA3XjUZACFDut7UuAEqPZepikCuTcprJBVAcSJREzIBeaYSC4kSGAs2BJU5IFLcQjt+sxNAqr55kwOx947iBrvVCRYwpBuDQusVLFWyFCmCVcEwCg8JVsPPK1GwEjxesNZJv6dyHtID6dYP8UnUCvPAemHBGiA+jD6CVgilD8+tWyfSPRiYXwVJDNNkydPUzvrRmeBZvFdArqSTDSCJ3ALcvDp0JBHWjTK8pb0Qvx7N35CkXo0yFRq1qZAgVaJkYiA7H3AA) + format('woff2'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, + U+01AF-01B0, U+1EA0-1EF9, U+20AB; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Roboto; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) + format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, + U+FE2E-FE2F; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+1F00-1FFF; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0370-03FF; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, + U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: Fira Code; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff;base64,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) + format('woff'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, + U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, + U+FEFF, U+FFFD; +} +.graphiql-container * { + box-sizing: border-box; + font-variant-ligatures: none; +} +.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, .graphiql-dialog, .graphiql-dialog-overlay, .graphiql-tooltip, [data-radix-popper-content-wrapper] { + --color-primary: 320, 95%, 43%; + --color-secondary: 242, 51%, 61%; + --color-tertiary: 188, 100%, 36%; + --color-info: 208, 100%, 46%; + --color-success: 158, 60%, 42%; + --color-warning: 36, 100%, 41%; + --color-error: 13, 93%, 58%; + --color-neutral: 219, 28%, 32%; + --color-base: 219, 28%, 100%; + --alpha-secondary: .76; + --alpha-tertiary: .5; + --alpha-background-heavy: .15; + --alpha-background-medium: .1; + --alpha-background-light: .07; + --font-family: "Roboto", sans-serif; + --font-family-mono: "Fira Code", monospace; + --font-size-hint: calc(12rem / 16); + --font-size-inline-code: calc(13rem / 16); + --font-size-body: calc(15rem / 16); + --font-size-h4: calc(18rem / 16); + --font-size-h3: calc(22rem / 16); + --font-size-h2: calc(29rem / 16); + --font-weight-regular: 400; + --font-weight-medium: 500; + --line-height: 1.5; + --px-2: 2px; + --px-4: 4px; + --px-6: 6px; + --px-8: 8px; + --px-10: 10px; + --px-12: 12px; + --px-16: 16px; + --px-20: 20px; + --px-24: 24px; + --border-radius-2: 2px; + --border-radius-4: 4px; + --border-radius-8: 8px; + --border-radius-12: 12px; + --popover-box-shadow: 0px 6px 20px #3b4c6a21, 0px 1.34018px 4.46726px #3b4c6a14, 0px .399006px 1.33002px #3b4c6a0d; + --popover-border: none; + --sidebar-width: 60px; + --toolbar-width: 40px; + --session-header-height: 38.5px; +} +@media (prefers-color-scheme: dark) { + body:not(.graphiql-light) .graphiql-container, body:not(.graphiql-light) .CodeMirror-info, body:not(.graphiql-light) .CodeMirror-lint-tooltip, body:not(.graphiql-light) .graphiql-dialog, body:not(.graphiql-light) .graphiql-dialog-overlay, body:not(.graphiql-light) .graphiql-tooltip, body:not(.graphiql-light) [data-radix-popper-content-wrapper] { + --color-primary: 338, 100%, 67%; + --color-secondary: 243, 100%, 77%; + --color-tertiary: 188, 100%, 44%; + --color-info: 208, 100%, 72%; + --color-success: 158, 100%, 42%; + --color-warning: 30, 100%, 80%; + --color-error: 13, 100%, 58%; + --color-neutral: 219, 29%, 78%; + --color-base: 219, 29%, 18%; + --popover-box-shadow: none; + --popover-border: 1px solid hsl(var(--color-neutral)); + } +} +body.graphiql-dark .graphiql-container, body.graphiql-dark .CodeMirror-info, body.graphiql-dark .CodeMirror-lint-tooltip, body.graphiql-dark .graphiql-dialog, body.graphiql-dark .graphiql-dialog-overlay, body.graphiql-dark .graphiql-tooltip, body.graphiql-dark [data-radix-popper-content-wrapper] { + --color-primary: 338, 100%, 67%; + --color-secondary: 243, 100%, 77%; + --color-tertiary: 188, 100%, 44%; + --color-info: 208, 100%, 72%; + --color-success: 158, 100%, 42%; + --color-warning: 30, 100%, 80%; + --color-error: 13, 100%, 58%; + --color-neutral: 219, 29%, 78%; + --color-base: 219, 29%, 18%; + --popover-box-shadow: none; + --popover-border: 1px solid hsl(var(--color-neutral)); +} +:is(.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, .graphiql-dialog) { + color: hsl(var(--color-neutral)); + font-family: var(--font-family); + font-size: var(--font-size-body); + font-weight: var(--font-weight-regular); + line-height: var(--line-height); +} +:is(.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, .graphiql-dialog):-webkit-any(button) { + color: hsl(var(--color-neutral)); + font-family: var(--font-family); + font-size: var(--font-size-body); + font-weight: var(--font-weight-regular); + line-height: var(--line-height); +} +:is(.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, .graphiql-dialog):-moz-any(button) { + color: hsl(var(--color-neutral)); + font-family: var(--font-family); + font-size: var(--font-size-body); + font-weight: var(--font-weight-regular); + line-height: var(--line-height); +} +:is(.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, .graphiql-dialog):is(button) { + color: hsl(var(--color-neutral)); + font-family: var(--font-family); + font-size: var(--font-size-body); + font-weight: var(--font-weight-regular); + line-height: var(--line-height); +} +:is(.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, .graphiql-dialog) input { + color: hsl(var(--color-neutral)); + font-family: var(--font-family); + font-size: var(--font-size-caption); +} +:is(.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, .graphiql-dialog) input::placeholder { + color: hsla(var(--color-neutral), var(--alpha-secondary)); +} +:is(.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, .graphiql-dialog) a { + color: hsl(var(--color-primary)); +} +:is(.graphiql-container, .CodeMirror-info, .CodeMirror-lint-tooltip, .graphiql-dialog) a:focus { + outline: hsl(var(--color-primary)) auto 1px; +} +.CodeMirror { + color: #000; + direction: ltr; + height: 300px; + font-family: monospace; +} +.CodeMirror-lines { + padding: 4px 0; +} +.CodeMirror pre.CodeMirror-line, .CodeMirror pre.CodeMirror-line-like { + padding: 0 4px; +} +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: #fff; +} +.CodeMirror-gutters { + white-space: nowrap; + background-color: #f7f7f7; + border-right: 1px solid #ddd; +} +.CodeMirror-linenumber { + text-align: right; + color: #999; + white-space: nowrap; + min-width: 20px; + padding: 0 3px 0 5px; +} +.CodeMirror-guttermarker { + color: #000; +} +.CodeMirror-guttermarker-subtle { + color: #999; +} +.CodeMirror-cursor { + border-left: 1px solid #000; + border-right: none; + width: 0; +} +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.cm-fat-cursor .CodeMirror-cursor { + background: #7e7; + width: auto; + border: 0 !important; +} +.cm-fat-cursor div.CodeMirror-cursors { + z-index: 1; +} +.cm-fat-cursor .CodeMirror-line::selection, .cm-fat-cursor .CodeMirror-line > span::selection, .cm-fat-cursor .CodeMirror-line > span > span::selection { + background: none; +} +.cm-fat-cursor .CodeMirror-line::-moz-selection { + background: none; +} +.cm-fat-cursor .CodeMirror-line > span::-moz-selection { + background: none; +} +.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { + background: none; +} +.cm-fat-cursor { + caret-color: #0000; +} +@keyframes blink { + 0% { + } + + 50% { + background-color: #0000; + } + + 100% { + } +} +.cm-tab { + -webkit-text-decoration: inherit; + text-decoration: inherit; + display: inline-block; +} +.CodeMirror-rulers { + position: absolute; + top: -50px; + bottom: 0; + left: 0; + right: 0; + overflow: hidden; +} +.CodeMirror-ruler { + border-left: 1px solid #ccc; + position: absolute; + top: 0; + bottom: 0; +} +.cm-s-default .cm-header { + color: #00f; +} +.cm-s-default .cm-quote { + color: #090; +} +.cm-negative { + color: #d44; +} +.cm-positive { + color: #292; +} +.cm-header, .cm-strong { + font-weight: bold; +} +.cm-em { + font-style: italic; +} +.cm-link { + text-decoration: underline; +} +.cm-strikethrough { + text-decoration: line-through; +} +.cm-s-default .cm-keyword { + color: #708; +} +.cm-s-default .cm-atom { + color: #219; +} +.cm-s-default .cm-number { + color: #164; +} +.cm-s-default .cm-def { + color: #00f; +} +.cm-s-default .cm-variable-2 { + color: #05a; +} +.cm-s-default .cm-variable-3, .cm-s-default .cm-type { + color: #085; +} +.cm-s-default .cm-comment { + color: #a50; +} +.cm-s-default .cm-string { + color: #a11; +} +.cm-s-default .cm-string-2 { + color: #f50; +} +.cm-s-default .cm-meta, .cm-s-default .cm-qualifier { + color: #555; +} +.cm-s-default .cm-builtin { + color: #30a; +} +.cm-s-default .cm-bracket { + color: #997; +} +.cm-s-default .cm-tag { + color: #170; +} +.cm-s-default .cm-attribute { + color: #00c; +} +.cm-s-default .cm-hr { + color: #999; +} +.cm-s-default .cm-link { + color: #00c; +} +.cm-s-default .cm-error, .cm-invalidchar { + color: red; +} +.CodeMirror-composing { + border-bottom: 2px solid; +} +div.CodeMirror span.CodeMirror-matchingbracket { + color: #0b0; +} +div.CodeMirror span.CodeMirror-nonmatchingbracket { + color: #a22; +} +.CodeMirror-matchingtag { + background: #ff96004d; +} +.CodeMirror-activeline-background { + background: #e8f2ff; +} +.CodeMirror { + background: #fff; + position: relative; + overflow: hidden; +} +.CodeMirror-scroll { + z-index: 0; + outline: none; + height: 100%; + margin-bottom: -50px; + margin-right: -50px; + padding-bottom: 50px; + position: relative; + overflow: scroll !important; +} +.CodeMirror-sizer { + border-right: 50px solid #0000; + position: relative; +} +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + z-index: 6; + outline: none; + display: none; + position: absolute; +} +.CodeMirror-vscrollbar { + top: 0; + right: 0; + overflow: hidden scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; + left: 0; + overflow: scroll hidden; +} +.CodeMirror-scrollbar-filler { + bottom: 0; + right: 0; +} +.CodeMirror-gutter-filler { + bottom: 0; + left: 0; +} +.CodeMirror-gutters { + z-index: 3; + min-height: 100%; + position: absolute; + top: 0; + left: 0; +} +.CodeMirror-gutter { + white-space: normal; + vertical-align: top; + height: 100%; + margin-bottom: -50px; + display: inline-block; +} +.CodeMirror-gutter-wrapper { + z-index: 4; + position: absolute; + background: none !important; + border: none !important; +} +.CodeMirror-gutter-background { + z-index: 4; + position: absolute; + top: 0; + bottom: 0; +} +.CodeMirror-gutter-elt { + cursor: default; + z-index: 4; + position: absolute; +} +.CodeMirror-gutter-wrapper ::selection { + background-color: #0000; +} +.CodeMirror-gutter-wrapper ::selection { + background-color: #0000; +} +.CodeMirror-lines { + cursor: text; + min-height: 1px; +} +.CodeMirror pre.CodeMirror-line, .CodeMirror pre.CodeMirror-line-like { + font-family: inherit; + font-size: inherit; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + -webkit-tap-highlight-color: transparent; + -webkit-font-variant-ligatures: contextual; + font-variant-ligatures: contextual; + background: none; + border-width: 0; + border-radius: 0; + margin: 0; + position: relative; + overflow: visible; +} +.CodeMirror-wrap pre.CodeMirror-line, .CodeMirror-wrap pre.CodeMirror-line-like { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} +.CodeMirror-linebackground { + z-index: 0; + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.CodeMirror-linewidget { + z-index: 2; + padding: .1px; + position: relative; +} +.CodeMirror-rtl pre { + direction: rtl; +} +.CodeMirror-code { + outline: none; +} +.CodeMirror-scroll, .CodeMirror-sizer, .CodeMirror-gutter, .CodeMirror-gutters, .CodeMirror-linenumber { + box-sizing: content-box; +} +.CodeMirror-measure { + visibility: hidden; + width: 100%; + height: 0; + position: absolute; + overflow: hidden; +} +.CodeMirror-cursor { + pointer-events: none; + position: absolute; +} +.CodeMirror-measure pre { + position: static; +} +div.CodeMirror-cursors { + visibility: hidden; + z-index: 3; + position: relative; +} +div.CodeMirror-dragcursors, .CodeMirror-focused div.CodeMirror-cursors { + visibility: visible; +} +.CodeMirror-selected { + background: #d9d9d9; +} +.CodeMirror-focused .CodeMirror-selected { + background: #d7d4f0; +} +.CodeMirror-crosshair { + cursor: crosshair; +} +.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { + background: #d7d4f0; +} +.CodeMirror-line::-moz-selection { + background: #d7d4f0; +} +.CodeMirror-line > span::-moz-selection { + background: #d7d4f0; +} +.CodeMirror-line > span > span::-moz-selection { + background: #d7d4f0; +} +.cm-searching { + background-color: #ff06; +} +.cm-force-border { + padding-right: .1px; +} +@media print { + .CodeMirror div.CodeMirror-cursors { + visibility: hidden; + } +} +.cm-tab-wrap-hack:after { + content: ""; +} +span.CodeMirror-selectedtext { + background: none; +} +.graphiql-container .CodeMirror { + width: 100%; + height: 100%; + font-family: var(--font-family-mono); + position: absolute; +} +.graphiql-container .CodeMirror, .graphiql-container .CodeMirror-gutters { + background: none; + background-color: var(--editor-background, hsl(var(--color-base))); +} +.graphiql-container .CodeMirror-linenumber { + padding: 0; +} +.graphiql-container .CodeMirror-gutters { + border: none; +} +.cm-s-graphiql { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); +} +.cm-s-graphiql .cm-keyword { + color: hsl(var(--color-primary)); +} +.cm-s-graphiql .cm-def { + color: hsl(var(--color-tertiary)); +} +.cm-s-graphiql .cm-punctuation { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); +} +.cm-s-graphiql .cm-variable { + color: hsl(var(--color-secondary)); +} +.cm-s-graphiql .cm-atom { + color: hsl(var(--color-tertiary)); +} +.cm-s-graphiql .cm-number { + color: hsl(var(--color-success)); +} +.cm-s-graphiql .cm-string { + color: hsl(var(--color-warning)); +} +.cm-s-graphiql .cm-builtin { + color: hsl(var(--color-success)); +} +.cm-s-graphiql .cm-string-2 { + color: hsl(var(--color-secondary)); +} +.cm-s-graphiql .cm-attribute { + color: hsl(var(--color-tertiary)); +} +.cm-s-graphiql .cm-meta { + color: hsl(var(--color-tertiary)); +} +.cm-s-graphiql .cm-property { + color: hsl(var(--color-info)); +} +.cm-s-graphiql .cm-qualifier { + color: hsl(var(--color-secondary)); +} +.cm-s-graphiql .cm-comment { + color: hsla(var(--color-neutral), var(--alpha-secondary)); +} +.cm-s-graphiql .cm-ws { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); +} +.cm-s-graphiql .cm-invalidchar { + color: hsl(var(--color-error)); +} +.cm-s-graphiql .CodeMirror-cursor { + border-left: 2px solid hsla(var(--color-neutral), var(--alpha-secondary)); +} +.cm-s-graphiql .CodeMirror-linenumber { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); +} +.graphiql-container div.CodeMirror span.CodeMirror-matchingbracket, .graphiql-container div.CodeMirror span.CodeMirror-nonmatchingbracket { + color: hsl(var(--color-warning)); +} +.graphiql-container .CodeMirror-selected, .graphiql-container .CodeMirror-focused .CodeMirror-selected { + background: hsla(var(--color-neutral), var(--alpha-background-heavy)); +} +.graphiql-container .CodeMirror-dialog { + background: inherit; + color: inherit; + padding: var(--px-2) var(--px-6); + z-index: 6; + position: absolute; + left: 0; + right: 0; + overflow: hidden; +} +.graphiql-container .CodeMirror-dialog-top { + border-bottom: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); + padding-bottom: var(--px-12); + top: 0; +} +.graphiql-container .CodeMirror-dialog-bottom { + border-top: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); + padding-top: var(--px-12); + bottom: 0; +} +.graphiql-container .CodeMirror-search-hint { + display: none; +} +.graphiql-container .CodeMirror-dialog input { + border: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); + border-radius: var(--border-radius-4); + padding: var(--px-4); +} +.graphiql-container .CodeMirror-dialog input:focus { + outline: hsl(var(--color-primary)) solid 2px; +} +.graphiql-container .cm-searching { + background-color: hsla(var(--color-warning), var(--alpha-background-light)); + padding-top: .5px; + padding-bottom: 1.5px; +} +.CodeMirror-foldmarker { + color: #00f; + text-shadow: 1px 1px 2px #b9f, -1px -1px 2px #b9f, 1px -1px 2px #b9f, -1px 1px 2px #b9f; + cursor: pointer; + font-family: arial; + line-height: .3; +} +.CodeMirror-foldgutter-open, .CodeMirror-foldgutter-folded { + cursor: pointer; +} +.CodeMirror-foldgutter-open:after { + content: "▾"; +} +.CodeMirror-foldgutter-folded:after { + content: "▸"; +} +.CodeMirror-foldgutter { + width: var(--px-12); +} +.CodeMirror-foldmarker { + background-color: hsl(var(--color-info)); + border-radius: var(--border-radius-4); + color: hsl(var(--color-base)); + margin: 0 var(--px-4); + padding: 0 var(--px-8); + text-shadow: none; + font-family: inherit; +} +.CodeMirror-foldgutter-open, .CodeMirror-foldgutter-folded { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); +} +:is(.CodeMirror-foldgutter-open, .CodeMirror-foldgutter-folded):after { + margin: 0 var(--px-2); +} +.graphiql-editor { + width: 100%; + height: 100%; + position: relative; +} +.graphiql-editor.hidden { + visibility: hidden; + position: absolute; + top: -9999px; + left: -9999px; +} +.CodeMirror-lint-markers { + width: 16px; +} +.CodeMirror-lint-tooltip { + color: #000; + white-space: pre; + white-space: pre-wrap; + z-index: 100; + opacity: 0; + -o-transition: opacity .4s; + background-color: #ffd; + border: 1px solid #000; + border-radius: 4px; + max-width: 600px; + padding: 2px 5px; + font-family: monospace; + font-size: 10pt; + transition: opacity .4s; + position: fixed; + overflow: hidden; +} +.CodeMirror-lint-mark { + background-position: 0 100%; + background-repeat: repeat-x; +} +.CodeMirror-lint-marker { + cursor: pointer; + vertical-align: middle; + background-position: center; + background-repeat: no-repeat; + width: 16px; + height: 16px; + display: inline-block; + position: relative; +} +.CodeMirror-lint-message { + background-position: 0 0; + background-repeat: no-repeat; + padding-left: 18px; +} +.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); +} +.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); +} +.CodeMirror-lint-marker-multiple { + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); + background-position: 100% 100%; + background-repeat: no-repeat; + width: 100%; + height: 100%; +} +.CodeMirror-lint-line-error { + background-color: #b74c5114; +} +.CodeMirror-lint-line-warning { + background-color: #ffd3001a; +} +.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { + background-position: 0 95%; + background-repeat: repeat-x; + background-size: 10px 3px; +} +.cm-s-graphiql .CodeMirror-lint-mark-error { + color: hsl(var(--color-error)); +} +.CodeMirror-lint-mark-error { + background-image: linear-gradient(45deg, transparent 65%, hsl(var(--color-error)) 80%, transparent 90%), linear-gradient(135deg, transparent 5%, hsl(var(--color-error)) 15%, transparent 25%), linear-gradient(135deg, transparent 45%, hsl(var(--color-error)) 55%, transparent 65%), linear-gradient(45deg, transparent 25%, hsl(var(--color-error)) 35%, transparent 50%); +} +.cm-s-graphiql .CodeMirror-lint-mark-warning { + color: hsl(var(--color-warning)); +} +.CodeMirror-lint-mark-warning { + background-image: linear-gradient(45deg, transparent 65%, hsl(var(--color-warning)) 80%, transparent 90%), linear-gradient(135deg, transparent 5%, hsl(var(--color-warning)) 15%, transparent 25%), linear-gradient(135deg, transparent 45%, hsl(var(--color-warning)) 55%, transparent 65%), linear-gradient(45deg, transparent 25%, hsl(var(--color-warning)) 35%, transparent 50%); +} +.CodeMirror-lint-tooltip { + background-color: hsl(var(--color-base)); + border: var(--popover-border); + border-radius: var(--border-radius-8); + box-shadow: var(--popover-box-shadow); + font-size: var(--font-size-body); + font-family: var(--font-family); + max-width: 600px; + padding: var(--px-12); + overflow: hidden; +} +.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { + background-image: none; + padding: 0; +} +.CodeMirror-lint-message-error { + color: hsl(var(--color-error)); +} +.CodeMirror-lint-message-warning { + color: hsl(var(--color-warning)); +} +.CodeMirror-hints { + z-index: 10; + background: #fff; + border: 1px solid silver; + border-radius: 3px; + max-height: 20em; + margin: 0; + padding: 2px; + font-family: monospace; + font-size: 90%; + list-style: none; + position: absolute; + overflow: hidden auto; + box-shadow: 2px 3px 5px #0003; +} +.CodeMirror-hint { + white-space: pre; + color: #000; + cursor: pointer; + border-radius: 2px; + margin: 0; + padding: 0 4px; +} +li.CodeMirror-hint-active { + color: #fff; + background: #08f; +} +.CodeMirror-hints { + background: hsl(var(--color-base)); + border: var(--popover-border); + border-radius: var(--border-radius-8); + box-shadow: var(--popover-box-shadow); + font-family: var(--font-family); + font-size: var(--font-size-body); + grid-template-columns: auto fit-content(300px); + max-height: 264px; + padding: 0; + display: grid; +} +.CodeMirror-hint { + border-radius: var(--border-radius-4); + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin: var(--px-4); + grid-column: 1 / 2; + padding: var(--px-6) var(--px-8) !important; +} +.CodeMirror-hint:not(:first-child) { + margin-top: 0; +} +li.CodeMirror-hint-active { + background: hsla(var(--color-primary), var(--alpha-background-medium)); + color: hsl(var(--color-primary)); +} +.CodeMirror-hint-information { + border-left: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); + max-height: 264px; + padding: var(--px-12); + grid-area: 1 / 2 / 99999 / 3; + overflow: auto; +} +.CodeMirror-hint-information-header { + align-items: baseline; + display: flex; +} +.CodeMirror-hint-information-field-name { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} +.CodeMirror-hint-information-type-name-pill { + border: 1px solid hsla(var(--color-neutral), var(--alpha-tertiary)); + border-radius: var(--border-radius-4); + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin-left: var(--px-6); + padding: var(--px-4); +} +.CodeMirror-hint-information-type-name { + color: inherit; + text-decoration: none; +} +.CodeMirror-hint-information-type-name:hover { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} +.CodeMirror-hint-information-description { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin-top: var(--px-12); +} +.CodeMirror-info { + background-color: hsl(var(--color-base)); + border: var(--popover-border); + border-radius: var(--border-radius-8); + box-shadow: var(--popover-box-shadow); + color: hsl(var(--color-neutral)); + opacity: 0; + max-width: 400px; + max-height: 300px; + padding: var(--px-12); + z-index: 10; + transition: opacity .15s; + position: fixed; + overflow: auto; +} +.CodeMirror-info a { + color: inherit; + text-decoration: none; +} +.CodeMirror-info a:hover { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} +.CodeMirror-info .CodeMirror-info-header { + align-items: baseline; + display: flex; +} +.CodeMirror-info .CodeMirror-info-header > .type-name { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} +.CodeMirror-info .CodeMirror-info-header > .field-name { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} +.CodeMirror-info .CodeMirror-info-header > .arg-name { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} +.CodeMirror-info .CodeMirror-info-header > .directive-name { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} +.CodeMirror-info .CodeMirror-info-header > .enum-value { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} +.CodeMirror-info .type-name-pill { + border: 1px solid hsla(var(--color-neutral), var(--alpha-tertiary)); + border-radius: var(--border-radius-4); + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin-left: var(--px-6); + padding: var(--px-4); +} +.CodeMirror-info .info-description { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin-top: var(--px-12); + overflow: hidden; +} +.CodeMirror-jump-token { + cursor: pointer; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} +.auto-inserted-leaf.cm-property { + border-radius: var(--border-radius-4); + padding: var(--px-2); + animation-name: insertionFade; + animation-duration: 6s; +} +@keyframes insertionFade { + from, to { + background-color: none; + } + + 15%, 85% { + background-color: hsla(var(--color-warning), var(--alpha-background-light)); + } +} +.graphiql-un-styled, button.graphiql-un-styled { + all: unset; + border-radius: var(--border-radius-4); + cursor: pointer; +} +:is(.graphiql-un-styled, button.graphiql-un-styled):hover { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); +} +:is(.graphiql-un-styled, button.graphiql-un-styled):active { + background-color: hsla(var(--color-neutral), var(--alpha-background-medium)); +} +:is(.graphiql-un-styled, button.graphiql-un-styled):focus { + outline: hsla(var(--color-neutral), var(--alpha-background-heavy)) auto 1px; +} +.graphiql-button, button.graphiql-button { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + border-radius: var(--border-radius-4); + color: hsl(var(--color-neutral)); + cursor: pointer; + font-size: var(--font-size-body); + padding: var(--px-8) var(--px-12); + border: none; +} +:is(.graphiql-button, button.graphiql-button):hover { + background-color: hsla(var(--color-neutral), var(--alpha-background-medium)); +} +:is(.graphiql-button, button.graphiql-button):active { + background-color: hsla(var(--color-neutral), var(--alpha-background-medium)); +} +:is(.graphiql-button, button.graphiql-button):focus { + outline: hsla(var(--color-neutral), var(--alpha-background-heavy)) auto 1px; +} +:is(.graphiql-button, button.graphiql-button).graphiql-button-success { + background-color: hsla(var(--color-success), var(--alpha-background-heavy)); +} +:is(.graphiql-button, button.graphiql-button).graphiql-button-error { + background-color: hsla(var(--color-error), var(--alpha-background-heavy)); +} +.graphiql-button-group { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + border-radius: calc(var(--border-radius-4) + var(--px-4)); + padding: var(--px-4); + display: flex; +} +.graphiql-button-group > button.graphiql-button { + background-color: #0000; +} +.graphiql-button-group > button.graphiql-button:hover { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); +} +.graphiql-button-group > button.graphiql-button.active { + background-color: hsl(var(--color-base)); + cursor: default; +} +.graphiql-button-group > * + * { + margin-left: var(--px-8); +} +.graphiql-dialog-overlay { + background-color: hsla(var(--color-neutral), var(--alpha-background-heavy)); + z-index: 10; + position: fixed; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.graphiql-dialog { + background-color: hsl(var(--color-base)); + border: var(--popover-border); + border-radius: var(--border-radius-12); + box-shadow: var(--popover-box-shadow); + max-width: 80vw; + max-height: 80vh; + width: unset; + z-index: 10; + margin: 0; + padding: 0; + position: fixed; + top: 50%; + left: 50%; + overflow: auto; + transform: translate(-50%, -50%); +} +.graphiql-dialog-close > svg { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + height: var(--px-12); + padding: var(--px-12); + width: var(--px-12); + display: block; +} +.graphiql-dropdown-content { + background-color: hsl(var(--color-base)); + border: var(--popover-border); + border-radius: var(--border-radius-8); + box-shadow: var(--popover-box-shadow); + font-size: inherit; + max-width: 250px; + padding: var(--px-4); + font-family: var(--font-family); + color: hsl(var(--color-neutral)); + max-height: min(calc(var(--radix-dropdown-menu-content-available-height) - 10px), 400px); + overflow-y: auto; +} +.graphiql-dropdown-item { + border-radius: var(--border-radius-4); + font-size: inherit; + margin: var(--px-4); + padding: var(--px-6) var(--px-8); + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + line-height: var(--line-height); + outline: none; + overflow: hidden; +} +.graphiql-dropdown-item[data-selected] { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + color: inherit; +} +.graphiql-dropdown-item[data-current-nav] { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + color: inherit; +} +.graphiql-dropdown-item:hover { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + color: inherit; +} +.graphiql-dropdown-item:not(:first-child) { + margin-top: 0; +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) blockquote { + padding-left: var(--px-8); + margin-left: 0; + margin-right: 0; +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) code { + border-radius: var(--border-radius-4); + font-family: var(--font-family-mono); + font-size: var(--font-size-inline-code); +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) pre { + border-radius: var(--border-radius-4); + font-family: var(--font-family-mono); + font-size: var(--font-size-inline-code); +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) code { + padding: var(--px-2); +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) pre { + padding: var(--px-6) var(--px-8); + overflow: auto; +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) pre code { + background-color: initial; + border-radius: 0; + padding: 0; +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) ol { + padding-left: var(--px-16); +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) ul { + padding-left: var(--px-16); +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) ol { + list-style-type: decimal; +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) ul { + list-style-type: disc; +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) img { + border-radius: var(--border-radius-4); + max-width: 100%; + max-height: 120px; +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) > :first-child { + margin-top: 0; +} +:is(.graphiql-markdown-description, .graphiql-markdown-deprecation, .CodeMirror-hint-information-description, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-description, .CodeMirror-info .info-deprecation) > :last-child { + margin-bottom: 0; +} +:is(.graphiql-markdown-description, .CodeMirror-hint-information-description, .CodeMirror-info .info-description) a { + color: hsl(var(--color-primary)); + text-decoration: none; +} +:is(.graphiql-markdown-description, .CodeMirror-hint-information-description, .CodeMirror-info .info-description) a:hover { + text-decoration: underline; +} +:is(.graphiql-markdown-description, .CodeMirror-hint-information-description, .CodeMirror-info .info-description) blockquote { + border-left: 1.5px solid hsla(var(--color-neutral), var(--alpha-tertiary)); +} +:is(.graphiql-markdown-description, .CodeMirror-hint-information-description, .CodeMirror-info .info-description) code { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + color: hsl(var(--color-neutral)); +} +:is(.graphiql-markdown-description, .CodeMirror-hint-information-description, .CodeMirror-info .info-description) pre { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + color: hsl(var(--color-neutral)); +} +:is(.graphiql-markdown-description, .CodeMirror-hint-information-description, .CodeMirror-info .info-description) > * { + margin: var(--px-12) 0; +} +:is(.graphiql-markdown-deprecation, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-deprecation) a { + color: hsl(var(--color-warning)); + text-decoration: underline; +} +:is(.graphiql-markdown-deprecation, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-deprecation) blockquote { + border-left: 1.5px solid hsl(var(--color-warning)); +} +:is(.graphiql-markdown-deprecation, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-deprecation) code { + background-color: hsla(var(--color-warning), var(--alpha-background-heavy)); +} +:is(.graphiql-markdown-deprecation, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-deprecation) pre { + background-color: hsla(var(--color-warning), var(--alpha-background-heavy)); +} +:is(.graphiql-markdown-deprecation, .CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-deprecation) > * { + margin: var(--px-8) 0; +} +.graphiql-markdown-preview > :not(:first-child) { + display: none; +} +.CodeMirror-hint-information-deprecation, .CodeMirror-info .info-deprecation { + background-color: hsla(var(--color-warning), var(--alpha-background-light)); + border: 1px solid hsl(var(--color-warning)); + border-radius: var(--border-radius-4); + color: hsl(var(--color-warning)); + margin-top: var(--px-12); + padding: var(--px-6) var(--px-8); +} +.CodeMirror-hint-information-deprecation-label, .CodeMirror-info .info-deprecation-label { + font-size: var(--font-size-hint); + font-weight: var(--font-weight-medium); +} +.CodeMirror-hint-information-deprecation-reason, .CodeMirror-info .info-deprecation-reason { + margin-top: var(--px-6); +} +.graphiql-spinner { + height: 56px; + margin: auto; + margin-top: var(--px-16); + width: 56px; +} +.graphiql-spinner:after { + border: 4px solid #0000; + border-top: 4px solid hsla(var(--color-neutral), var(--alpha-tertiary)); + content: ""; + vertical-align: middle; + border-radius: 100%; + width: 46px; + height: 46px; + animation: .8s linear infinite rotation; + display: inline-block; +} +@keyframes rotation { + from { + transform: rotate(0); + } + + to { + transform: rotate(360deg); + } +} +.graphiql-tabs { + --bg: hsl(var(--color-base)); + align-items: center; + gap: var(--px-8); + border-top-left-radius: var(--border-radius-8); + margin: 0; + padding: 2px 0; + list-style: none; + display: flex; + overflow: auto; +} +.no-scrollbar { + scrollbar-width: none; + -ms-overflow-style: none; +} +.no-scrollbar::-webkit-scrollbar { + display: none; +} +.graphiql-tabs, .graphiql-tab { + min-width: 0; +} +.graphiql-tab { + border-radius: var(--border-radius-8) var(--border-radius-8) 0 0; + background: hsla(var(--color-neutral), var(--alpha-background-light)); + flex-shrink: 0; + display: flex; + position: relative; +} +.graphiql-tab:not(:focus-within) { + transform: none !important; +} +.graphiql-tab:hover { + background: var(--bg); + color: hsl(var(--color-neutral)); +} +.graphiql-tab:hover .graphiql-tab-close { + display: block; +} +.graphiql-tab:focus-within { + background: var(--bg); + color: hsl(var(--color-neutral)); +} +.graphiql-tab:focus-within .graphiql-tab-close { + display: block; +} +.graphiql-tab.graphiql-tab-active { + background: var(--bg); + color: hsl(var(--color-neutral)); +} +.graphiql-tab.graphiql-tab-active .graphiql-tab-close { + display: block; +} +.graphiql-tab .graphiql-tab-button { + border-radius: var(--border-radius-8) var(--border-radius-8) 0 0; + padding: var(--px-4) 28px var(--px-4) var(--px-8); +} +.graphiql-tab .graphiql-tab-button:hover { + background: none; +} +.graphiql-tab .graphiql-tab-close { + right: min(var(--px-4), 5%); + background: var(--bg); + padding: var(--px-6); + line-height: 0; + display: none; + position: absolute; + top: 50%; + transform: translateY(-50%); +} +.graphiql-tab .graphiql-tab-close > svg { + height: var(--px-8); + width: var(--px-8); +} +.graphiql-tab .graphiql-tab-close:hover { + background: var(--bg); + color: hsl(var(--color-neutral)); + overflow: hidden; +} +.graphiql-tab .graphiql-tab-close:hover:before { + content: ""; + z-index: -1; + background: hsla(var(--color-neutral), .3); + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; +} +.graphiql-tooltip { + background: hsl(var(--color-base)); + border: var(--popover-border); + border-radius: var(--border-radius-4); + box-shadow: var(--popover-box-shadow); + color: hsl(var(--color-neutral)); + font-size: inherit; + padding: var(--px-4) var(--px-6); + font-family: var(--font-family); +} +button.graphiql-toolbar-button { + height: var(--toolbar-width); + width: var(--toolbar-width); + justify-content: center; + align-items: center; + display: flex; +} +button.graphiql-toolbar-button.error { + background: hsla(var(--color-error), var(--alpha-background-heavy)); +} +.graphiql-execute-button-wrapper { + position: relative; +} +button.graphiql-execute-button { + background-color: hsl(var(--color-primary)); + border-radius: var(--border-radius-8); + cursor: pointer; + height: var(--toolbar-width); + width: var(--toolbar-width); + border: none; + padding: 0; +} +button.graphiql-execute-button:hover { + background-color: hsla(var(--color-primary), .9); +} +button.graphiql-execute-button:active { + background-color: hsla(var(--color-primary), .8); +} +button.graphiql-execute-button:focus { + outline: hsla(var(--color-primary), .8) auto 1px; +} +button.graphiql-execute-button > svg { + color: #fff; + height: var(--px-16); + width: var(--px-16); + margin: auto; + display: block; +} +button.graphiql-toolbar-menu { + height: var(--toolbar-width); + width: var(--toolbar-width); + display: block; +} +.graphiql-history-header { + font-size: var(--font-size-h2); + font-weight: var(--font-weight-medium); + justify-content: space-between; + align-items: center; + display: flex; +} +.graphiql-history-header button { + font-size: var(--font-size-inline-code); + padding: var(--px-6) var(--px-10); +} +.graphiql-history-items { + margin: var(--px-16) 0 0; + padding: 0; + list-style: none; +} +.graphiql-history-item { + border-radius: var(--border-radius-4); + color: hsla(var(--color-neutral), var(--alpha-secondary)); + font-size: var(--font-size-inline-code); + font-family: var(--font-family-mono); + height: 34px; + display: flex; +} +.graphiql-history-item:hover { + color: hsl(var(--color-neutral)); + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); +} +.graphiql-history-item:not(:first-child) { + margin-top: var(--px-4); +} +.graphiql-history-item.editable { + background-color: hsla(var(--color-primary), var(--alpha-background-medium)); +} +.graphiql-history-item.editable > input { + padding: 0 var(--px-10); + background: none; + border: none; + outline: none; + flex: 1; + width: 100%; + margin: 0; +} +.graphiql-history-item.editable > input::placeholder { + color: hsla(var(--color-neutral), var(--alpha-secondary)); +} +.graphiql-history-item.editable > button { + color: hsl(var(--color-primary)); + padding: 0 var(--px-10); +} +.graphiql-history-item.editable > button:active { + background-color: hsla(var(--color-primary), var(--alpha-background-heavy)); +} +.graphiql-history-item.editable > button:focus { + outline: hsl(var(--color-primary)) auto 1px; +} +.graphiql-history-item.editable > button > svg { + display: block; +} +button.graphiql-history-item-label { + padding: var(--px-8) var(--px-10); + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + overflow: hidden; +} +button.graphiql-history-item-action { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + padding: var(--px-8) var(--px-6); + align-items: center; + display: flex; +} +button.graphiql-history-item-action:hover { + color: hsl(var(--color-neutral)); +} +button.graphiql-history-item-action > svg { + width: 14px; + height: 14px; +} +.graphiql-history-item-spacer { + height: var(--px-16); +} +.graphiql-doc-explorer-default-value { + color: hsl(var(--color-success)); +} +a.graphiql-doc-explorer-type-name { + color: hsl(var(--color-warning)); + text-decoration: none; +} +a.graphiql-doc-explorer-type-name:hover { + text-decoration: underline; +} +a.graphiql-doc-explorer-type-name:focus { + outline: hsl(var(--color-warning)) auto 1px; +} +.graphiql-doc-explorer-argument > * + * { + margin-top: var(--px-12); +} +.graphiql-doc-explorer-argument-name { + color: hsl(var(--color-secondary)); +} +.graphiql-doc-explorer-argument-deprecation { + background-color: hsla(var(--color-warning), var(--alpha-background-light)); + border: 1px solid hsl(var(--color-warning)); + border-radius: var(--border-radius-4); + color: hsl(var(--color-warning)); + padding: var(--px-8); +} +.graphiql-doc-explorer-argument-deprecation-label { + font-size: var(--font-size-hint); + font-weight: var(--font-weight-medium); +} +.graphiql-doc-explorer-deprecation { + background-color: hsla(var(--color-warning), var(--alpha-background-light)); + border: 1px solid hsl(var(--color-warning)); + border-radius: var(--px-4); + color: hsl(var(--color-warning)); + padding: var(--px-8); +} +.graphiql-doc-explorer-deprecation-label { + font-size: var(--font-size-hint); + font-weight: var(--font-weight-medium); +} +.graphiql-doc-explorer-directive { + color: hsl(var(--color-secondary)); +} +.graphiql-doc-explorer-section-title { + font-size: var(--font-size-hint); + font-weight: var(--font-weight-medium); + align-items: center; + line-height: 1; + display: flex; +} +.graphiql-doc-explorer-section-title > svg { + height: var(--px-16); + margin-right: var(--px-8); + width: var(--px-16); +} +.graphiql-doc-explorer-section-content { + margin-left: var(--px-8); + margin-top: var(--px-16); +} +.graphiql-doc-explorer-section-content > * + * { + margin-top: var(--px-16); +} +.graphiql-doc-explorer-root-type { + color: hsl(var(--color-info)); +} +.graphiql-doc-explorer-search { + color: hsla(var(--color-neutral), var(--alpha-secondary)); +} +.graphiql-doc-explorer-search:not([data-state="idle"]) { + border: var(--popover-border); + border-radius: var(--border-radius-4); + box-shadow: var(--popover-box-shadow); + color: hsl(var(--color-neutral)); +} +.graphiql-doc-explorer-search:not([data-state="idle"]) .graphiql-doc-explorer-search-input { + background: hsl(var(--color-base)); +} +.graphiql-doc-explorer-search-input { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + border-radius: var(--border-radius-4); + padding: var(--px-8) var(--px-12); + align-items: center; + display: flex; +} +.graphiql-doc-explorer-search [role="combobox"] { + margin-left: var(--px-4); + background-color: #0000; + border: none; + width: 100%; +} +.graphiql-doc-explorer-search [role="combobox"]:focus { + outline: none; +} +.graphiql-doc-explorer-search [role="listbox"] { + background-color: hsl(var(--color-base)); + border-bottom-left-radius: var(--border-radius-4); + border-bottom-right-radius: var(--border-radius-4); + border: none; + border-top: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); + max-height: 400px; + font-size: var(--font-size-body); + padding: var(--px-4); + margin: 0; + position: relative; + overflow-y: auto; +} +.graphiql-doc-explorer-search [role="option"] { + border-radius: var(--border-radius-4); + color: hsla(var(--color-neutral), var(--alpha-secondary)); + padding: var(--px-8) var(--px-12); + text-overflow: ellipsis; + white-space: nowrap; + cursor: pointer; + overflow-x: hidden; +} +.graphiql-doc-explorer-search [role="option"][data-headlessui-state="active"] { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); +} +.graphiql-doc-explorer-search [role="option"]:hover { + background-color: hsla(var(--color-neutral), var(--alpha-background-medium)); +} +.graphiql-doc-explorer-search [role="option"][data-headlessui-state="active"]:hover { + background-color: hsla(var(--color-neutral), var(--alpha-background-heavy)); +} +.graphiql-doc-explorer-search [role="option"] + :is(.graphiql-doc-explorer-search [role="option"]) { + margin-top: var(--px-4); +} +.graphiql-doc-explorer-search-type { + color: hsl(var(--color-info)); +} +.graphiql-doc-explorer-search-field { + color: hsl(var(--color-warning)); +} +.graphiql-doc-explorer-search-argument { + color: hsl(var(--color-secondary)); +} +.graphiql-doc-explorer-search-divider { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + font-size: var(--font-size-hint); + font-weight: var(--font-weight-medium); + margin-top: var(--px-8); + padding: var(--px-8) var(--px-12); +} +.graphiql-doc-explorer-search-empty { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + padding: var(--px-8) var(--px-12); +} +a.graphiql-doc-explorer-field-name { + color: hsl(var(--color-info)); + text-decoration: none; +} +a.graphiql-doc-explorer-field-name:hover { + text-decoration: underline; +} +a.graphiql-doc-explorer-field-name:focus { + outline: hsl(var(--color-info)) auto 1px; +} +.graphiql-doc-explorer-item > :not(:first-child) { + margin-top: var(--px-12); +} +.graphiql-doc-explorer-argument-multiple { + margin-left: var(--px-8); +} +.graphiql-doc-explorer-enum-value { + color: hsl(var(--color-info)); +} +.graphiql-doc-explorer-header { + justify-content: space-between; + display: flex; + position: relative; +} +.graphiql-doc-explorer-header:focus-within .graphiql-doc-explorer-title { + visibility: hidden; +} +.graphiql-doc-explorer-header:focus-within .graphiql-doc-explorer-back:not(:focus) { + color: #0000; +} +.graphiql-doc-explorer-header-content { + flex-direction: column; + min-width: 0; + display: flex; +} +.graphiql-doc-explorer-search { + position: absolute; + top: 0; + right: 0; +} +.graphiql-doc-explorer-search:focus-within { + left: 0; +} +.graphiql-doc-explorer-search:not(:focus-within) [role="combobox"] { + width: 5ch; + height: 24px; +} +.graphiql-doc-explorer-search [role="combobox"]:focus { + width: 100%; +} +a.graphiql-doc-explorer-back { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + align-items: center; + text-decoration: none; + display: flex; +} +a.graphiql-doc-explorer-back:hover { + text-decoration: underline; +} +a.graphiql-doc-explorer-back:focus { + outline: hsla(var(--color-neutral), var(--alpha-secondary)) auto 1px; +} +a.graphiql-doc-explorer-back:focus + .graphiql-doc-explorer-title { + visibility: unset; +} +a.graphiql-doc-explorer-back > svg { + height: var(--px-8); + margin-right: var(--px-8); + width: var(--px-8); +} +.graphiql-doc-explorer-title { + font-weight: var(--font-weight-medium); + font-size: var(--font-size-h2); + text-overflow: ellipsis; + white-space: nowrap; + overflow-x: hidden; +} +.graphiql-doc-explorer-title:not(:first-child) { + font-size: var(--font-size-h3); + margin-top: var(--px-8); +} +.graphiql-doc-explorer-content > * { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + margin-top: var(--px-20); +} +.graphiql-doc-explorer-error { + background-color: hsla(var(--color-error), var(--alpha-background-heavy)); + border: 1px solid hsl(var(--color-error)); + border-radius: var(--border-radius-8); + color: hsl(var(--color-error)); + padding: var(--px-8) var(--px-12); +} +/* Everything */ +.graphiql-container { + background-color: hsl(var(--color-base)); + display: flex; + height: 100%; + margin: 0; + overflow: hidden; + width: 100%; +} +/* The sidebar */ +.graphiql-container .graphiql-sidebar { + display: flex; + flex-direction: column; + padding: var(--px-8); + width: var(--sidebar-width); + gap: var(--px-8); + overflow-y: auto; +} +.graphiql-container .graphiql-sidebar > button { + display: flex; + align-items: center; + justify-content: center; + color: hsla(var(--color-neutral), var(--alpha-secondary)); + height: calc(var(--sidebar-width) - (2 * var(--px-8))); + width: calc(var(--sidebar-width) - (2 * var(--px-8))); + flex-shrink: 0; +} +.graphiql-container .graphiql-sidebar button.active { + color: hsl(var(--color-neutral)); +} +.graphiql-container .graphiql-sidebar button > svg { + height: var(--px-20); + width: var(--px-20); +} +/* The main content, i.e. everything except the sidebar */ +.graphiql-container .graphiql-main { + display: flex; + flex: 1; + min-width: 0; +} +/* The current session and tabs */ +.graphiql-container .graphiql-sessions { + background-color: hsla(var(--color-neutral), var(--alpha-background-light)); + /* Adding the 8px of padding to the inner border radius of the query editor */ + border-radius: calc(var(--border-radius-12) + var(--px-8)); + display: flex; + flex-direction: column; + flex: 1; + max-height: 100%; + margin: var(--px-16); + margin-left: 0; + min-width: 0; +} +/* The session header containing tabs and the logo */ +.graphiql-container .graphiql-session-header { + height: var(--session-header-height); + align-items: center; + display: flex; + padding: var(--px-8) var(--px-8) 0; + gap: var(--px-8); +} +/* The button to add a new tab */ +button.graphiql-tab-add { + padding: var(--px-4); + + & > svg { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + display: block; + height: var(--px-16); + width: var(--px-16); + } +} +/* The GraphiQL logo */ +.graphiql-container .graphiql-logo { + margin-left: auto; + color: hsla(var(--color-neutral), var(--alpha-secondary)); + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} +/* Undo default link styling for the default GraphiQL logo link */ +.graphiql-container .graphiql-logo .graphiql-logo-link { + color: hsla(var(--color-neutral), var(--alpha-secondary)); + text-decoration: none; + + &:focus { + outline: hsla(var(--color-neutral), var(--alpha-background-heavy)) auto 1px; + } +} +/* The editor of the session */ +.graphiql-container #graphiql-session { + display: flex; + flex: 1; + padding: 0 var(--px-8) var(--px-8); +} +/* All editors (query, variable, headers) */ +.graphiql-container .graphiql-editors { + background-color: hsl(var(--color-base)); + border-radius: 0 0 var(--border-radius-12) var(--border-radius-12); + box-shadow: var(--popover-box-shadow); + display: flex; + flex: 1; + flex-direction: column; +} +/* The query editor and the toolbar */ +.graphiql-container .graphiql-query-editor { + border-bottom: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + padding: var(--px-16); + column-gap: var(--px-16); + display: flex; + width: 100%; +} +/* The vertical toolbar next to the query editor */ +.graphiql-container .graphiql-toolbar { + width: var(--toolbar-width); + display: flex; + flex-direction: column; + gap: var(--px-8); +} +.graphiql-container .graphiql-toolbar > button { + flex-shrink: 0; +} +/* The toolbar icons */ +.graphiql-toolbar-icon { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + display: block; + height: calc(var(--toolbar-width) - (var(--px-8) * 2)); + width: calc(var(--toolbar-width) - (var(--px-8) * 2)); +} +/* The tab bar for editor tools */ +.graphiql-container .graphiql-editor-tools { + cursor: row-resize; + display: flex; + width: 100%; + column-gap: var(--px-8); + padding: var(--px-8); +} +.graphiql-container .graphiql-editor-tools button { + color: hsla(var(--color-neutral), var(--alpha-secondary)); +} +.graphiql-container .graphiql-editor-tools button.active { + color: hsl(var(--color-neutral)); +} +/* The tab buttons to switch between editor tools */ +.graphiql-container + .graphiql-editor-tools + > button:not(.graphiql-toggle-editor-tools) { + padding: var(--px-8) var(--px-12); +} +.graphiql-container .graphiql-editor-tools .graphiql-toggle-editor-tools { + margin-left: auto; +} +/* An editor tool, e.g. variable or header editor */ +.graphiql-container .graphiql-editor-tool { + flex: 1; + padding: var(--px-16); +} +/** + * The way CodeMirror editors are styled they overflow their containing + * element. For some OS-browser-combinations this might cause overlap issues, + * setting the position of this to `relative` makes sure this element will + * always be on top of any editors. + */ +.graphiql-container .graphiql-toolbar, +.graphiql-container .graphiql-editor-tools, +.graphiql-container .graphiql-editor-tool { + position: relative; +} +/* The response view */ +.graphiql-container .graphiql-response { + --editor-background: transparent; + display: flex; + width: 100%; + flex-direction: column; +} +/* The results editor wrapping container */ +.graphiql-container .graphiql-response .result-window { + position: relative; + flex: 1; +} +/* The footer below the response view */ +.graphiql-container .graphiql-footer { + border-top: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); +} +/* The plugin container */ +.graphiql-container .graphiql-plugin { + border-left: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + flex: 1; + overflow-y: auto; + padding: var(--px-16); +} +/* Generic drag bar for horizontal resizing */ +.graphiql-horizontal-drag-bar { + width: var(--px-12); + cursor: col-resize; +} +.graphiql-horizontal-drag-bar:hover::after { + border: var(--px-2) solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + border-radius: var(--border-radius-2); + content: ''; + display: block; + height: 25%; + margin: 0 auto; + position: relative; + /* (100% - 25%) / 2 = 37.5% */ + top: 37.5%; + width: 0; +} +.graphiql-container .graphiql-chevron-icon { + color: hsla(var(--color-neutral), var(--alpha-tertiary)); + display: block; + height: var(--px-12); + margin: var(--px-12); + width: var(--px-12); +} +/* Generic spin animation */ +.graphiql-spin { + animation: spin 0.8s linear 0s infinite; +} +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} +/* The header of the settings dialog */ +.graphiql-dialog .graphiql-dialog-header { + align-items: center; + display: flex; + justify-content: space-between; + padding: var(--px-24); +} +/* The title of the settings dialog */ +.graphiql-dialog .graphiql-dialog-title { + font-size: var(--font-size-h3); + font-weight: var(--font-weight-medium); + margin: 0; +} +/* A section inside the settings dialog */ +.graphiql-dialog .graphiql-dialog-section { + align-items: center; + border-top: 1px solid + hsla(var(--color-neutral), var(--alpha-background-heavy)); + display: flex; + justify-content: space-between; + padding: var(--px-24); +} +.graphiql-dialog .graphiql-dialog-section > :not(:first-child) { + margin-left: var(--px-24); +} +/* The section title in the settings dialog */ +.graphiql-dialog .graphiql-dialog-section-title { + font-size: var(--font-size-h4); + font-weight: var(--font-weight-medium); +} +/* The section caption in the settings dialog */ +.graphiql-dialog .graphiql-dialog-section-caption { + color: hsla(var(--color-neutral), var(--alpha-secondary)); +} +.graphiql-dialog .graphiql-warning-text { + color: hsl(var(--color-warning)); + font-weight: var(--font-weight-medium); +} +.graphiql-dialog .graphiql-table { + border-collapse: collapse; + width: 100%; +} +.graphiql-dialog .graphiql-table :is(th, td) { + border: 1px solid hsla(var(--color-neutral), var(--alpha-background-heavy)); + padding: var(--px-8) var(--px-12); +} +/* A single key the short-key dialog */ +.graphiql-dialog .graphiql-key { + background-color: hsla(var(--color-neutral), var(--alpha-background-medium)); + border-radius: var(--border-radius-4); + padding: var(--px-4); +} +/* Avoid showing native tooltips for icons with titles */ +.graphiql-container svg { + pointer-events: none; +} diff --git a/src/Laravel/public/graphiql/graphiql.min.js b/src/Laravel/public/graphiql/graphiql.min.js new file mode 100644 index 00000000000..49a5c6cb4cb --- /dev/null +++ b/src/Laravel/public/graphiql/graphiql.min.js @@ -0,0 +1,32 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("react"),require("react-dom")):"function"==typeof define&&define.amd?define(["react","react-dom"],t):(e="undefined"!=typeof globalThis?globalThis:e||self).GraphiQL=t(e.React,e.ReactDOM)}(this,(function(e,t){"use strict";function n(e){const t=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(e)for(const n in e)if("default"!==n){const r=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,r.get?r:{enumerable:!0,get:()=>e[n]})}return t.default=e,Object.freeze(t)}function r(e,t){for(var n=0;nr[t]})}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}const i=n(e),o=n(t);function s(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var a,l,c={exports:{}},u={};var d,f,p=(l||(l=1,c.exports=function(){if(a)return u;a=1;var t=e,n=Symbol.for("react.element"),r=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,o=t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,s={key:!0,ref:!0,__self:!0,__source:!0};function l(e,t,r){var a,l={},c=null,u=null;for(a in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)i.call(t,a)&&!s.hasOwnProperty(a)&&(l[a]=t[a]);if(e&&e.defaultProps)for(a in t=e.defaultProps)void 0===l[a]&&(l[a]=t[a]);return{$$typeof:n,type:e,key:c,ref:u,props:l,_owner:o.current}}return u.Fragment=r,u.jsx=l,u.jsxs=l,u}()),c.exports); +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @lightSyntaxTransform + * @noflow + * @nolint + * @preventMunge + * @preserve-invariant-messages + */var h=function(){if(f)return d;f=1;var t,n=Object.create,r=Object.defineProperty,i=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,l=(e,t,n,s)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let l of o(t))a.call(e,l)||l===n||r(e,l,{get:()=>t[l],enumerable:!(s=i(t,l))||s.enumerable});return e},c={};((e,t)=>{for(var n in t)r(e,n,{get:t[n],enumerable:!0})})(c,{$dispatcherGuard:()=>S,$makeReadOnly:()=>_,$reset:()=>k,$structuralCheck:()=>O,c:()=>E,clearRenderCounterRegistry:()=>D,renderCounterRegistry:()=>N,useRenderCounter:()=>A}),t=c,d=l(r({},"__esModule",{value:!0}),t);var u,p,h=((e,t,i)=>(i=null!=e?n(s(e)):{},l(e&&e.__esModule?i:r(i,"default",{value:e,enumerable:!0}),e)))(e),{useRef:m,useEffect:g,isValidElement:v}=h,y=null!=(u=h.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE)?u:h.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,b=Symbol.for("react.memo_cache_sentinel"),E="function"==typeof(null==(p=h.__COMPILER_RUNTIME)?void 0:p.c)?h.__COMPILER_RUNTIME.c:function(e){return h.useMemo((()=>{const t=new Array(e);for(let n=0;n{x[e]=()=>{throw new Error(`[React] Unexpected React hook call (${e}) from a React compiled function. Check that all hooks are called directly and named according to convention ('use[A-Z]') `)}}));var w=null;function T(e){return y.ReactCurrentDispatcher.current=e,y.ReactCurrentDispatcher.current}x.useMemoCache=e=>{if(null==w)throw new Error("React Compiler internal invariant violation: unexpected null dispatcher");return w.useMemoCache(e)};var C=[];function S(e){const t=y.ReactCurrentDispatcher.current;if(0===e){if(C.push(t),1===C.length&&(w=t),t===x)throw new Error("[React] Unexpected call to custom hook or component from a React compiled function. Check that (1) all hooks are called directly and named according to convention ('use[A-Z]') and (2) components are returned as JSX instead of being directly invoked.");T(x)}else if(1===e){const e=C.pop();if(null==e)throw new Error("React Compiler internal error: unexpected null in guard stack");0===C.length&&(w=null),T(e)}else if(2===e)C.push(t),T(w);else{if(3!==e)throw new Error("React Compiler internal error: unreachable block"+e);{const e=C.pop();if(null==e)throw new Error("React Compiler internal error: unexpected null in guard stack");T(e)}}}function k(e){for(let t=0;t{e.count=0}))}function A(e){const t=m(null);null!=t.current&&(t.current.count+=1),g((()=>{if(null==t.current){const n={count:0};!function(e,t){let n=N.get(e);null==n&&(n=new Set,N.set(e,n)),n.add(t)}(e,n),t.current=n}return()=>{null!==t.current&&function(e,t){const n=N.get(e);null!=n&&n.delete(t)}(e,t.current)}}))}var I=new Set;function O(e,t,n,r,i,o){function s(e,t,s,a){const l=`${r}:${o} [${i}] ${n}${s} changed from ${e} to ${t} at depth ${a}`;I.has(l)||(I.add(l),console.error(l))}!function e(t,n,r,i){if(!(i>2)&&t!==n)if(typeof t!=typeof n)s("type "+typeof t,"type "+typeof n,r,i);else if("object"==typeof t){const o=Array.isArray(t),a=Array.isArray(n);if(null===t&&null!==n)s("null","type "+typeof n,r,i);else if(null===n)s("type "+typeof t,"null",r,i);else if(t instanceof Map)if(n instanceof Map)if(t.size!==n.size)s(`Map instance with size ${t.size}`,`Map instance with size ${n.size}`,r,i);else for(const[l,c]of t)n.has(l)?e(c,n.get(l),`${r}.get(${l})`,i+1):s(`Map instance with key ${l}`,`Map instance without key ${l}`,r,i);else s("Map instance","other value",r,i);else if(n instanceof Map)s("other value","Map instance",r,i);else if(t instanceof Set)if(n instanceof Set)if(t.size!==n.size)s(`Set instance with size ${t.size}`,`Set instance with size ${n.size}`,r,i);else for(const e of n)t.has(e)||s(`Set instance without element ${e}`,`Set instance with element ${e}`,r,i);else s("Set instance","other value",r,i);else if(n instanceof Set)s("other value","Set instance",r,i);else if(o||a)if(o!==a)s("type "+(o?"array":"object"),"type "+(a?"array":"object"),r,i);else if(t.length!==n.length)s(`array with length ${t.length}`,`array with length ${n.length}`,r,i);else for(let s=0;s(t=Symbol[e])?t:Symbol.for("Symbol."+e),w=(e,t,n)=>t in e?m(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,T=(e,t)=>{for(var n in t||(t={}))b.call(t,n)&&w(e,n,t[n]);if(y)for(var n of y(t))E.call(t,n)&&w(e,n,t[n]);return e},C=(e,t)=>g(e,v(t)),S=function(e,t){this[0]=e,this[1]=t};function k(e){return"object"==typeof e&&null!==e&&"function"==typeof e.then}function _(e){return"object"==typeof e&&null!==e&&"subscribe"in e&&"function"==typeof e.subscribe}function N(e){return"object"==typeof e&&null!==e&&("AsyncGenerator"===e[Symbol.toStringTag]||Symbol.asyncIterator in e)}async function D(e){const t=await e;return N(t)?async function(e){var t;const n=null==(t=("return"in e?e:e[Symbol.asyncIterator]()).return)?void 0:t.bind(e),r=await("next"in e?e:e[Symbol.asyncIterator]()).next.bind(e)();return null==n||n(),r.value}(t):_(t)?(n=t,new Promise(((e,t)=>{const r=n.subscribe({next(t){e(t),r.unsubscribe()},error:t,complete(){t(new Error("no value resolved"))}})}))):t;var n}const A=Object.freeze({major:16,minor:11,patch:0,preReleaseTag:null});function I(e,t){if(!Boolean(e))throw new Error(t)}function O(e){return"function"==typeof(null==e?void 0:e.then)}function L(e){return"object"==typeof e&&null!==e}function M(e,t){if(!Boolean(e))throw new Error(null!=t?t:"Unexpected invariant triggered.")}const R=/\r\n|[\n\r]/g;function F(e,t){let n=0,r=1;for(const i of e.body.matchAll(R)){if("number"==typeof i.index||M(!1),i.index>=t)break;n=i.index+i[0].length,r+=1}return{line:r,column:t+1-n}}function P(e){return j(e.source,F(e.source,e.start))}function j(e,t){const n=e.locationOffset.column-1,r="".padStart(n)+e.body,i=t.line-1,o=e.locationOffset.line-1,s=t.line+o,a=1===t.line?n:0,l=t.column+a,c=`${e.name}:${s}:${l}\n`,u=r.split(/\r\n|[\n\r]/g),d=u[i];if(d.length>120){const e=Math.floor(l/80),t=l%80,n=[];for(let r=0;r["|",e])),["|","^".padStart(t)],["|",n[e+1]]])}return c+V([[s-1+" |",u[i-1]],[`${s} |`,d],["|","^".padStart(l)],[`${s+1} |`,u[i+1]]])}function V(e){const t=e.filter((([e,t])=>void 0!==t)),n=Math.max(...t.map((([e])=>e.length)));return t.map((([e,t])=>e.padStart(n)+(t?" "+t:""))).join("\n")}class B extends Error{constructor(e,...t){var n,r,i;const{nodes:o,source:s,positions:a,path:l,originalError:c,extensions:u}=function(e){const t=e[0];return null==t||"kind"in t||"length"in t?{nodes:t,source:e[1],positions:e[2],path:e[3],originalError:e[4],extensions:e[5]}:t}(t);super(e),this.name="GraphQLError",this.path=null!=l?l:void 0,this.originalError=null!=c?c:void 0,this.nodes=$(Array.isArray(o)?o:o?[o]:void 0);const d=$(null===(n=this.nodes)||void 0===n?void 0:n.map((e=>e.loc)).filter((e=>null!=e)));this.source=null!=s?s:null==d||null===(r=d[0])||void 0===r?void 0:r.source,this.positions=null!=a?a:null==d?void 0:d.map((e=>e.start)),this.locations=a&&s?a.map((e=>F(s,e))):null==d?void 0:d.map((e=>F(e.source,e.start)));const f=L(null==c?void 0:c.extensions)?null==c?void 0:c.extensions:void 0;this.extensions=null!==(i=null!=u?u:f)&&void 0!==i?i:Object.create(null),Object.defineProperties(this,{message:{writable:!0,enumerable:!0},name:{enumerable:!1},nodes:{enumerable:!1},source:{enumerable:!1},positions:{enumerable:!1},originalError:{enumerable:!1}}),null!=c&&c.stack?Object.defineProperty(this,"stack",{value:c.stack,writable:!0,configurable:!0}):Error.captureStackTrace?Error.captureStackTrace(this,B):Object.defineProperty(this,"stack",{value:Error().stack,writable:!0,configurable:!0})}get[Symbol.toStringTag](){return"GraphQLError"}toString(){let e=this.message;if(this.nodes)for(const t of this.nodes)t.loc&&(e+="\n\n"+P(t.loc));else if(this.source&&this.locations)for(const t of this.locations)e+="\n\n"+j(this.source,t);return e}toJSON(){const e={message:this.message};return null!=this.locations&&(e.locations=this.locations),null!=this.path&&(e.path=this.path),null!=this.extensions&&Object.keys(this.extensions).length>0&&(e.extensions=this.extensions),e}}function $(e){return void 0===e||0===e.length?void 0:e}function U(e,t,n){return new B(`Syntax Error: ${n}`,{source:e,positions:[t]})}let H=class{constructor(e,t,n){this.start=e.start,this.end=t.end,this.startToken=e,this.endToken=t,this.source=n}get[Symbol.toStringTag](){return"Location"}toJSON(){return{start:this.start,end:this.end}}},q=class{constructor(e,t,n,r,i,o){this.kind=e,this.start=t,this.end=n,this.line=r,this.column=i,this.value=o,this.prev=null,this.next=null}get[Symbol.toStringTag](){return"Token"}toJSON(){return{kind:this.kind,value:this.value,line:this.line,column:this.column}}};const W={Name:[],Document:["definitions"],OperationDefinition:["name","variableDefinitions","directives","selectionSet"],VariableDefinition:["variable","type","defaultValue","directives"],Variable:["name"],SelectionSet:["selections"],Field:["alias","name","arguments","directives","selectionSet"],Argument:["name","value"],FragmentSpread:["name","directives"],InlineFragment:["typeCondition","directives","selectionSet"],FragmentDefinition:["name","variableDefinitions","typeCondition","directives","selectionSet"],IntValue:[],FloatValue:[],StringValue:[],BooleanValue:[],NullValue:[],EnumValue:[],ListValue:["values"],ObjectValue:["fields"],ObjectField:["name","value"],Directive:["name","arguments"],NamedType:["name"],ListType:["type"],NonNullType:["type"],SchemaDefinition:["description","directives","operationTypes"],OperationTypeDefinition:["type"],ScalarTypeDefinition:["description","name","directives"],ObjectTypeDefinition:["description","name","interfaces","directives","fields"],FieldDefinition:["description","name","arguments","type","directives"],InputValueDefinition:["description","name","type","defaultValue","directives"],InterfaceTypeDefinition:["description","name","interfaces","directives","fields"],UnionTypeDefinition:["description","name","directives","types"],EnumTypeDefinition:["description","name","directives","values"],EnumValueDefinition:["description","name","directives"],InputObjectTypeDefinition:["description","name","directives","fields"],DirectiveDefinition:["description","name","arguments","locations"],SchemaExtension:["directives","operationTypes"],ScalarTypeExtension:["name","directives"],ObjectTypeExtension:["name","interfaces","directives","fields"],InterfaceTypeExtension:["name","interfaces","directives","fields"],UnionTypeExtension:["name","directives","types"],EnumTypeExtension:["name","directives","values"],InputObjectTypeExtension:["name","directives","fields"]},z=new Set(Object.keys(W));function G(e){const t=null==e?void 0:e.kind;return"string"==typeof t&&z.has(t)}var K,Y,Q,X,J,Z,ee,te;function ne(e){return 9===e||32===e}function re(e){return e>=48&&e<=57}function ie(e){return e>=97&&e<=122||e>=65&&e<=90}function oe(e){return ie(e)||95===e}function se(e){return ie(e)||re(e)||95===e}function ae(e){var t;let n=Number.MAX_SAFE_INTEGER,r=null,i=-1;for(let s=0;s0===t?e:e.slice(n))).slice(null!==(t=r)&&void 0!==t?t:0,i+1)}function le(e){let t=0;for(;t1&&r.slice(1).every((e=>0===e.length||ne(e.charCodeAt(0)))),s=n.endsWith('\\"""'),a=e.endsWith('"')&&!s,l=e.endsWith("\\"),c=a||l,u=!(null!=t&&t.minimize)&&(!i||e.length>70||c||o||s);let d="";const f=i&&ne(e.charCodeAt(0));return(u&&!f||o)&&(d+="\n"),d+=n,(u||c)&&(d+="\n"),'"""'+d+'"""'}(Y=K||(K={})).QUERY="query",Y.MUTATION="mutation",Y.SUBSCRIPTION="subscription",(X=Q||(Q={})).QUERY="QUERY",X.MUTATION="MUTATION",X.SUBSCRIPTION="SUBSCRIPTION",X.FIELD="FIELD",X.FRAGMENT_DEFINITION="FRAGMENT_DEFINITION",X.FRAGMENT_SPREAD="FRAGMENT_SPREAD",X.INLINE_FRAGMENT="INLINE_FRAGMENT",X.VARIABLE_DEFINITION="VARIABLE_DEFINITION",X.SCHEMA="SCHEMA",X.SCALAR="SCALAR",X.OBJECT="OBJECT",X.FIELD_DEFINITION="FIELD_DEFINITION",X.ARGUMENT_DEFINITION="ARGUMENT_DEFINITION",X.INTERFACE="INTERFACE",X.UNION="UNION",X.ENUM="ENUM",X.ENUM_VALUE="ENUM_VALUE",X.INPUT_OBJECT="INPUT_OBJECT",X.INPUT_FIELD_DEFINITION="INPUT_FIELD_DEFINITION",(Z=J||(J={})).NAME="Name",Z.DOCUMENT="Document",Z.OPERATION_DEFINITION="OperationDefinition",Z.VARIABLE_DEFINITION="VariableDefinition",Z.SELECTION_SET="SelectionSet",Z.FIELD="Field",Z.ARGUMENT="Argument",Z.FRAGMENT_SPREAD="FragmentSpread",Z.INLINE_FRAGMENT="InlineFragment",Z.FRAGMENT_DEFINITION="FragmentDefinition",Z.VARIABLE="Variable",Z.INT="IntValue",Z.FLOAT="FloatValue",Z.STRING="StringValue",Z.BOOLEAN="BooleanValue",Z.NULL="NullValue",Z.ENUM="EnumValue",Z.LIST="ListValue",Z.OBJECT="ObjectValue",Z.OBJECT_FIELD="ObjectField",Z.DIRECTIVE="Directive",Z.NAMED_TYPE="NamedType",Z.LIST_TYPE="ListType",Z.NON_NULL_TYPE="NonNullType",Z.SCHEMA_DEFINITION="SchemaDefinition",Z.OPERATION_TYPE_DEFINITION="OperationTypeDefinition",Z.SCALAR_TYPE_DEFINITION="ScalarTypeDefinition",Z.OBJECT_TYPE_DEFINITION="ObjectTypeDefinition",Z.FIELD_DEFINITION="FieldDefinition",Z.INPUT_VALUE_DEFINITION="InputValueDefinition",Z.INTERFACE_TYPE_DEFINITION="InterfaceTypeDefinition",Z.UNION_TYPE_DEFINITION="UnionTypeDefinition",Z.ENUM_TYPE_DEFINITION="EnumTypeDefinition",Z.ENUM_VALUE_DEFINITION="EnumValueDefinition",Z.INPUT_OBJECT_TYPE_DEFINITION="InputObjectTypeDefinition",Z.DIRECTIVE_DEFINITION="DirectiveDefinition",Z.SCHEMA_EXTENSION="SchemaExtension",Z.SCALAR_TYPE_EXTENSION="ScalarTypeExtension",Z.OBJECT_TYPE_EXTENSION="ObjectTypeExtension",Z.INTERFACE_TYPE_EXTENSION="InterfaceTypeExtension",Z.UNION_TYPE_EXTENSION="UnionTypeExtension",Z.ENUM_TYPE_EXTENSION="EnumTypeExtension",Z.INPUT_OBJECT_TYPE_EXTENSION="InputObjectTypeExtension",(te=ee||(ee={})).SOF="",te.EOF="",te.BANG="!",te.DOLLAR="$",te.AMP="&",te.PAREN_L="(",te.PAREN_R=")",te.SPREAD="...",te.COLON=":",te.EQUALS="=",te.AT="@",te.BRACKET_L="[",te.BRACKET_R="]",te.BRACE_L="{",te.PIPE="|",te.BRACE_R="}",te.NAME="Name",te.INT="Int",te.FLOAT="Float",te.STRING="String",te.BLOCK_STRING="BlockString",te.COMMENT="Comment";class de{constructor(e){const t=new q(ee.SOF,0,0,0,0);this.source=e,this.lastToken=t,this.token=t,this.line=1,this.lineStart=0}get[Symbol.toStringTag](){return"Lexer"}advance(){this.lastToken=this.token;return this.token=this.lookahead()}lookahead(){let e=this.token;if(e.kind!==ee.EOF)do{if(e.next)e=e.next;else{const t=be(this,e.end);e.next=t,t.prev=e,e=t}}while(e.kind===ee.COMMENT);return e}}function fe(e){return e===ee.BANG||e===ee.DOLLAR||e===ee.AMP||e===ee.PAREN_L||e===ee.PAREN_R||e===ee.SPREAD||e===ee.COLON||e===ee.EQUALS||e===ee.AT||e===ee.BRACKET_L||e===ee.BRACKET_R||e===ee.BRACE_L||e===ee.PIPE||e===ee.BRACE_R}function pe(e){return e>=0&&e<=55295||e>=57344&&e<=1114111}function he(e,t){return me(e.charCodeAt(t))&&ge(e.charCodeAt(t+1))}function me(e){return e>=55296&&e<=56319}function ge(e){return e>=56320&&e<=57343}function ve(e,t){const n=e.source.body.codePointAt(t);if(void 0===n)return ee.EOF;if(n>=32&&n<=126){const e=String.fromCodePoint(n);return'"'===e?"'\"'":`"${e}"`}return"U+"+n.toString(16).toUpperCase().padStart(4,"0")}function ye(e,t,n,r,i){const o=e.line,s=1+n-e.lineStart;return new q(t,n,r,o,s,i)}function be(e,t){const n=e.source.body,r=n.length;let i=t;for(;i=48&&e<=57?e-48:e>=65&&e<=70?e-55:e>=97&&e<=102?e-87:-1}function Ne(e,t){const n=e.source.body;switch(n.charCodeAt(t+1)){case 34:return{value:'"',size:2};case 92:return{value:"\\",size:2};case 47:return{value:"/",size:2};case 98:return{value:"\b",size:2};case 102:return{value:"\f",size:2};case 110:return{value:"\n",size:2};case 114:return{value:"\r",size:2};case 116:return{value:"\t",size:2}}throw U(e.source,t,`Invalid character escape sequence: "${n.slice(t,t+2)}".`)}function De(e,t){const n=e.source.body,r=n.length;let i=e.lineStart,o=t+3,s=o,a="";const l=[];for(;oOe)return"[Array]";const n=Math.min(Ie,e.length),r=e.length-n,i=[];for(let o=0;o1&&i.push(`... ${r} more items`);return"["+i.join(", ")+"]"}(e,n);return function(e,t){const n=Object.entries(e);if(0===n.length)return"{}";if(t.length>Oe)return"["+function(e){const t=Object.prototype.toString.call(e).replace(/^\[object /,"").replace(/]$/,"");if("Object"===t&&"function"==typeof e.constructor){const t=e.constructor.name;if("string"==typeof t&&""!==t)return t}return t}(e)+"]";const r=n.map((([e,n])=>e+": "+Me(n,t)));return"{ "+r.join(", ")+" }"}(e,n)}(e,t);default:return String(e)}}const Re=function(e,t){return e instanceof t};class Fe{constructor(e,t="GraphQL request",n={line:1,column:1}){"string"==typeof e||I(!1,`Body must be a string. Received: ${Le(e)}.`),this.body=e,this.name=t,this.locationOffset=n,this.locationOffset.line>0||I(!1,"line in locationOffset is 1-indexed and must be positive."),this.locationOffset.column>0||I(!1,"column in locationOffset is 1-indexed and must be positive.")}get[Symbol.toStringTag](){return"Source"}}function Pe(e){return Re(e,Fe)}function je(e,t){const n=new Be(e,t),r=n.parseDocument();return Object.defineProperty(r,"tokenCount",{enumerable:!1,value:n.tokenCount}),r}function Ve(e,t){const n=new Be(e,t);n.expectToken(ee.SOF);const r=n.parseValueLiteral(!1);return n.expectToken(ee.EOF),r}class Be{constructor(e,t={}){const n=Pe(e)?e:new Fe(e);this._lexer=new de(n),this._options=t,this._tokenCounter=0}get tokenCount(){return this._tokenCounter}parseName(){const e=this.expectToken(ee.NAME);return this.node(e,{kind:J.NAME,value:e.value})}parseDocument(){return this.node(this._lexer.token,{kind:J.DOCUMENT,definitions:this.many(ee.SOF,this.parseDefinition,ee.EOF)})}parseDefinition(){if(this.peek(ee.BRACE_L))return this.parseOperationDefinition();const e=this.peekDescription(),t=e?this._lexer.lookahead():this._lexer.token;if(t.kind===ee.NAME){switch(t.value){case"schema":return this.parseSchemaDefinition();case"scalar":return this.parseScalarTypeDefinition();case"type":return this.parseObjectTypeDefinition();case"interface":return this.parseInterfaceTypeDefinition();case"union":return this.parseUnionTypeDefinition();case"enum":return this.parseEnumTypeDefinition();case"input":return this.parseInputObjectTypeDefinition();case"directive":return this.parseDirectiveDefinition()}if(e)throw U(this._lexer.source,this._lexer.token.start,"Unexpected description, descriptions are supported only on type definitions.");switch(t.value){case"query":case"mutation":case"subscription":return this.parseOperationDefinition();case"fragment":return this.parseFragmentDefinition();case"extend":return this.parseTypeSystemExtension()}}throw this.unexpected(t)}parseOperationDefinition(){const e=this._lexer.token;if(this.peek(ee.BRACE_L))return this.node(e,{kind:J.OPERATION_DEFINITION,operation:K.QUERY,name:void 0,variableDefinitions:[],directives:[],selectionSet:this.parseSelectionSet()});const t=this.parseOperationType();let n;return this.peek(ee.NAME)&&(n=this.parseName()),this.node(e,{kind:J.OPERATION_DEFINITION,operation:t,name:n,variableDefinitions:this.parseVariableDefinitions(),directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet()})}parseOperationType(){const e=this.expectToken(ee.NAME);switch(e.value){case"query":return K.QUERY;case"mutation":return K.MUTATION;case"subscription":return K.SUBSCRIPTION}throw this.unexpected(e)}parseVariableDefinitions(){return this.optionalMany(ee.PAREN_L,this.parseVariableDefinition,ee.PAREN_R)}parseVariableDefinition(){return this.node(this._lexer.token,{kind:J.VARIABLE_DEFINITION,variable:this.parseVariable(),type:(this.expectToken(ee.COLON),this.parseTypeReference()),defaultValue:this.expectOptionalToken(ee.EQUALS)?this.parseConstValueLiteral():void 0,directives:this.parseConstDirectives()})}parseVariable(){const e=this._lexer.token;return this.expectToken(ee.DOLLAR),this.node(e,{kind:J.VARIABLE,name:this.parseName()})}parseSelectionSet(){return this.node(this._lexer.token,{kind:J.SELECTION_SET,selections:this.many(ee.BRACE_L,this.parseSelection,ee.BRACE_R)})}parseSelection(){return this.peek(ee.SPREAD)?this.parseFragment():this.parseField()}parseField(){const e=this._lexer.token,t=this.parseName();let n,r;return this.expectOptionalToken(ee.COLON)?(n=t,r=this.parseName()):r=t,this.node(e,{kind:J.FIELD,alias:n,name:r,arguments:this.parseArguments(!1),directives:this.parseDirectives(!1),selectionSet:this.peek(ee.BRACE_L)?this.parseSelectionSet():void 0})}parseArguments(e){const t=e?this.parseConstArgument:this.parseArgument;return this.optionalMany(ee.PAREN_L,t,ee.PAREN_R)}parseArgument(e=!1){const t=this._lexer.token,n=this.parseName();return this.expectToken(ee.COLON),this.node(t,{kind:J.ARGUMENT,name:n,value:this.parseValueLiteral(e)})}parseConstArgument(){return this.parseArgument(!0)}parseFragment(){const e=this._lexer.token;this.expectToken(ee.SPREAD);const t=this.expectOptionalKeyword("on");return!t&&this.peek(ee.NAME)?this.node(e,{kind:J.FRAGMENT_SPREAD,name:this.parseFragmentName(),directives:this.parseDirectives(!1)}):this.node(e,{kind:J.INLINE_FRAGMENT,typeCondition:t?this.parseNamedType():void 0,directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet()})}parseFragmentDefinition(){const e=this._lexer.token;return this.expectKeyword("fragment"),!0===this._options.allowLegacyFragmentVariables?this.node(e,{kind:J.FRAGMENT_DEFINITION,name:this.parseFragmentName(),variableDefinitions:this.parseVariableDefinitions(),typeCondition:(this.expectKeyword("on"),this.parseNamedType()),directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet()}):this.node(e,{kind:J.FRAGMENT_DEFINITION,name:this.parseFragmentName(),typeCondition:(this.expectKeyword("on"),this.parseNamedType()),directives:this.parseDirectives(!1),selectionSet:this.parseSelectionSet()})}parseFragmentName(){if("on"===this._lexer.token.value)throw this.unexpected();return this.parseName()}parseValueLiteral(e){const t=this._lexer.token;switch(t.kind){case ee.BRACKET_L:return this.parseList(e);case ee.BRACE_L:return this.parseObject(e);case ee.INT:return this.advanceLexer(),this.node(t,{kind:J.INT,value:t.value});case ee.FLOAT:return this.advanceLexer(),this.node(t,{kind:J.FLOAT,value:t.value});case ee.STRING:case ee.BLOCK_STRING:return this.parseStringLiteral();case ee.NAME:switch(this.advanceLexer(),t.value){case"true":return this.node(t,{kind:J.BOOLEAN,value:!0});case"false":return this.node(t,{kind:J.BOOLEAN,value:!1});case"null":return this.node(t,{kind:J.NULL});default:return this.node(t,{kind:J.ENUM,value:t.value})}case ee.DOLLAR:if(e){if(this.expectToken(ee.DOLLAR),this._lexer.token.kind===ee.NAME){const e=this._lexer.token.value;throw U(this._lexer.source,t.start,`Unexpected variable "$${e}" in constant value.`)}throw this.unexpected(t)}return this.parseVariable();default:throw this.unexpected()}}parseConstValueLiteral(){return this.parseValueLiteral(!0)}parseStringLiteral(){const e=this._lexer.token;return this.advanceLexer(),this.node(e,{kind:J.STRING,value:e.value,block:e.kind===ee.BLOCK_STRING})}parseList(e){return this.node(this._lexer.token,{kind:J.LIST,values:this.any(ee.BRACKET_L,(()=>this.parseValueLiteral(e)),ee.BRACKET_R)})}parseObject(e){return this.node(this._lexer.token,{kind:J.OBJECT,fields:this.any(ee.BRACE_L,(()=>this.parseObjectField(e)),ee.BRACE_R)})}parseObjectField(e){const t=this._lexer.token,n=this.parseName();return this.expectToken(ee.COLON),this.node(t,{kind:J.OBJECT_FIELD,name:n,value:this.parseValueLiteral(e)})}parseDirectives(e){const t=[];for(;this.peek(ee.AT);)t.push(this.parseDirective(e));return t}parseConstDirectives(){return this.parseDirectives(!0)}parseDirective(e){const t=this._lexer.token;return this.expectToken(ee.AT),this.node(t,{kind:J.DIRECTIVE,name:this.parseName(),arguments:this.parseArguments(e)})}parseTypeReference(){const e=this._lexer.token;let t;if(this.expectOptionalToken(ee.BRACKET_L)){const n=this.parseTypeReference();this.expectToken(ee.BRACKET_R),t=this.node(e,{kind:J.LIST_TYPE,type:n})}else t=this.parseNamedType();return this.expectOptionalToken(ee.BANG)?this.node(e,{kind:J.NON_NULL_TYPE,type:t}):t}parseNamedType(){return this.node(this._lexer.token,{kind:J.NAMED_TYPE,name:this.parseName()})}peekDescription(){return this.peek(ee.STRING)||this.peek(ee.BLOCK_STRING)}parseDescription(){if(this.peekDescription())return this.parseStringLiteral()}parseSchemaDefinition(){const e=this._lexer.token,t=this.parseDescription();this.expectKeyword("schema");const n=this.parseConstDirectives(),r=this.many(ee.BRACE_L,this.parseOperationTypeDefinition,ee.BRACE_R);return this.node(e,{kind:J.SCHEMA_DEFINITION,description:t,directives:n,operationTypes:r})}parseOperationTypeDefinition(){const e=this._lexer.token,t=this.parseOperationType();this.expectToken(ee.COLON);const n=this.parseNamedType();return this.node(e,{kind:J.OPERATION_TYPE_DEFINITION,operation:t,type:n})}parseScalarTypeDefinition(){const e=this._lexer.token,t=this.parseDescription();this.expectKeyword("scalar");const n=this.parseName(),r=this.parseConstDirectives();return this.node(e,{kind:J.SCALAR_TYPE_DEFINITION,description:t,name:n,directives:r})}parseObjectTypeDefinition(){const e=this._lexer.token,t=this.parseDescription();this.expectKeyword("type");const n=this.parseName(),r=this.parseImplementsInterfaces(),i=this.parseConstDirectives(),o=this.parseFieldsDefinition();return this.node(e,{kind:J.OBJECT_TYPE_DEFINITION,description:t,name:n,interfaces:r,directives:i,fields:o})}parseImplementsInterfaces(){return this.expectOptionalKeyword("implements")?this.delimitedMany(ee.AMP,this.parseNamedType):[]}parseFieldsDefinition(){return this.optionalMany(ee.BRACE_L,this.parseFieldDefinition,ee.BRACE_R)}parseFieldDefinition(){const e=this._lexer.token,t=this.parseDescription(),n=this.parseName(),r=this.parseArgumentDefs();this.expectToken(ee.COLON);const i=this.parseTypeReference(),o=this.parseConstDirectives();return this.node(e,{kind:J.FIELD_DEFINITION,description:t,name:n,arguments:r,type:i,directives:o})}parseArgumentDefs(){return this.optionalMany(ee.PAREN_L,this.parseInputValueDef,ee.PAREN_R)}parseInputValueDef(){const e=this._lexer.token,t=this.parseDescription(),n=this.parseName();this.expectToken(ee.COLON);const r=this.parseTypeReference();let i;this.expectOptionalToken(ee.EQUALS)&&(i=this.parseConstValueLiteral());const o=this.parseConstDirectives();return this.node(e,{kind:J.INPUT_VALUE_DEFINITION,description:t,name:n,type:r,defaultValue:i,directives:o})}parseInterfaceTypeDefinition(){const e=this._lexer.token,t=this.parseDescription();this.expectKeyword("interface");const n=this.parseName(),r=this.parseImplementsInterfaces(),i=this.parseConstDirectives(),o=this.parseFieldsDefinition();return this.node(e,{kind:J.INTERFACE_TYPE_DEFINITION,description:t,name:n,interfaces:r,directives:i,fields:o})}parseUnionTypeDefinition(){const e=this._lexer.token,t=this.parseDescription();this.expectKeyword("union");const n=this.parseName(),r=this.parseConstDirectives(),i=this.parseUnionMemberTypes();return this.node(e,{kind:J.UNION_TYPE_DEFINITION,description:t,name:n,directives:r,types:i})}parseUnionMemberTypes(){return this.expectOptionalToken(ee.EQUALS)?this.delimitedMany(ee.PIPE,this.parseNamedType):[]}parseEnumTypeDefinition(){const e=this._lexer.token,t=this.parseDescription();this.expectKeyword("enum");const n=this.parseName(),r=this.parseConstDirectives(),i=this.parseEnumValuesDefinition();return this.node(e,{kind:J.ENUM_TYPE_DEFINITION,description:t,name:n,directives:r,values:i})}parseEnumValuesDefinition(){return this.optionalMany(ee.BRACE_L,this.parseEnumValueDefinition,ee.BRACE_R)}parseEnumValueDefinition(){const e=this._lexer.token,t=this.parseDescription(),n=this.parseEnumValueName(),r=this.parseConstDirectives();return this.node(e,{kind:J.ENUM_VALUE_DEFINITION,description:t,name:n,directives:r})}parseEnumValueName(){if("true"===this._lexer.token.value||"false"===this._lexer.token.value||"null"===this._lexer.token.value)throw U(this._lexer.source,this._lexer.token.start,`${$e(this._lexer.token)} is reserved and cannot be used for an enum value.`);return this.parseName()}parseInputObjectTypeDefinition(){const e=this._lexer.token,t=this.parseDescription();this.expectKeyword("input");const n=this.parseName(),r=this.parseConstDirectives(),i=this.parseInputFieldsDefinition();return this.node(e,{kind:J.INPUT_OBJECT_TYPE_DEFINITION,description:t,name:n,directives:r,fields:i})}parseInputFieldsDefinition(){return this.optionalMany(ee.BRACE_L,this.parseInputValueDef,ee.BRACE_R)}parseTypeSystemExtension(){const e=this._lexer.lookahead();if(e.kind===ee.NAME)switch(e.value){case"schema":return this.parseSchemaExtension();case"scalar":return this.parseScalarTypeExtension();case"type":return this.parseObjectTypeExtension();case"interface":return this.parseInterfaceTypeExtension();case"union":return this.parseUnionTypeExtension();case"enum":return this.parseEnumTypeExtension();case"input":return this.parseInputObjectTypeExtension()}throw this.unexpected(e)}parseSchemaExtension(){const e=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("schema");const t=this.parseConstDirectives(),n=this.optionalMany(ee.BRACE_L,this.parseOperationTypeDefinition,ee.BRACE_R);if(0===t.length&&0===n.length)throw this.unexpected();return this.node(e,{kind:J.SCHEMA_EXTENSION,directives:t,operationTypes:n})}parseScalarTypeExtension(){const e=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("scalar");const t=this.parseName(),n=this.parseConstDirectives();if(0===n.length)throw this.unexpected();return this.node(e,{kind:J.SCALAR_TYPE_EXTENSION,name:t,directives:n})}parseObjectTypeExtension(){const e=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("type");const t=this.parseName(),n=this.parseImplementsInterfaces(),r=this.parseConstDirectives(),i=this.parseFieldsDefinition();if(0===n.length&&0===r.length&&0===i.length)throw this.unexpected();return this.node(e,{kind:J.OBJECT_TYPE_EXTENSION,name:t,interfaces:n,directives:r,fields:i})}parseInterfaceTypeExtension(){const e=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("interface");const t=this.parseName(),n=this.parseImplementsInterfaces(),r=this.parseConstDirectives(),i=this.parseFieldsDefinition();if(0===n.length&&0===r.length&&0===i.length)throw this.unexpected();return this.node(e,{kind:J.INTERFACE_TYPE_EXTENSION,name:t,interfaces:n,directives:r,fields:i})}parseUnionTypeExtension(){const e=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("union");const t=this.parseName(),n=this.parseConstDirectives(),r=this.parseUnionMemberTypes();if(0===n.length&&0===r.length)throw this.unexpected();return this.node(e,{kind:J.UNION_TYPE_EXTENSION,name:t,directives:n,types:r})}parseEnumTypeExtension(){const e=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("enum");const t=this.parseName(),n=this.parseConstDirectives(),r=this.parseEnumValuesDefinition();if(0===n.length&&0===r.length)throw this.unexpected();return this.node(e,{kind:J.ENUM_TYPE_EXTENSION,name:t,directives:n,values:r})}parseInputObjectTypeExtension(){const e=this._lexer.token;this.expectKeyword("extend"),this.expectKeyword("input");const t=this.parseName(),n=this.parseConstDirectives(),r=this.parseInputFieldsDefinition();if(0===n.length&&0===r.length)throw this.unexpected();return this.node(e,{kind:J.INPUT_OBJECT_TYPE_EXTENSION,name:t,directives:n,fields:r})}parseDirectiveDefinition(){const e=this._lexer.token,t=this.parseDescription();this.expectKeyword("directive"),this.expectToken(ee.AT);const n=this.parseName(),r=this.parseArgumentDefs(),i=this.expectOptionalKeyword("repeatable");this.expectKeyword("on");const o=this.parseDirectiveLocations();return this.node(e,{kind:J.DIRECTIVE_DEFINITION,description:t,name:n,arguments:r,repeatable:i,locations:o})}parseDirectiveLocations(){return this.delimitedMany(ee.PIPE,this.parseDirectiveLocation)}parseDirectiveLocation(){const e=this._lexer.token,t=this.parseName();if(Object.prototype.hasOwnProperty.call(Q,t.value))return t;throw this.unexpected(e)}node(e,t){return!0!==this._options.noLocation&&(t.loc=new H(e,this._lexer.lastToken,this._lexer.source)),t}peek(e){return this._lexer.token.kind===e}expectToken(e){const t=this._lexer.token;if(t.kind===e)return this.advanceLexer(),t;throw U(this._lexer.source,t.start,`Expected ${Ue(e)}, found ${$e(t)}.`)}expectOptionalToken(e){return this._lexer.token.kind===e&&(this.advanceLexer(),!0)}expectKeyword(e){const t=this._lexer.token;if(t.kind!==ee.NAME||t.value!==e)throw U(this._lexer.source,t.start,`Expected "${e}", found ${$e(t)}.`);this.advanceLexer()}expectOptionalKeyword(e){const t=this._lexer.token;return t.kind===ee.NAME&&t.value===e&&(this.advanceLexer(),!0)}unexpected(e){const t=null!=e?e:this._lexer.token;return U(this._lexer.source,t.start,`Unexpected ${$e(t)}.`)}any(e,t,n){this.expectToken(e);const r=[];for(;!this.expectOptionalToken(n);)r.push(t.call(this));return r}optionalMany(e,t,n){if(this.expectOptionalToken(e)){const e=[];do{e.push(t.call(this))}while(!this.expectOptionalToken(n));return e}return[]}many(e,t,n){this.expectToken(e);const r=[];do{r.push(t.call(this))}while(!this.expectOptionalToken(n));return r}delimitedMany(e,t){this.expectOptionalToken(e);const n=[];do{n.push(t.call(this))}while(this.expectOptionalToken(e));return n}advanceLexer(){const{maxTokens:e}=this._options,t=this._lexer.advance();if(t.kind!==ee.EOF&&(++this._tokenCounter,void 0!==e&&this._tokenCounter>e))throw U(this._lexer.source,t.start,`Document contains more that ${e} tokens. Parsing aborted.`)}}function $e(e){const t=e.value;return Ue(e.kind)+(null!=t?` "${t}"`:"")}function Ue(e){return fe(e)?`"${e}"`:e}const He=5;function qe(e,t){const[n,r]=t?[e,t]:[void 0,e];let i=" Did you mean ";n&&(i+=n+" ");const o=r.map((e=>`"${e}"`));switch(o.length){case 0:return"";case 1:return i+o[0]+"?";case 2:return i+o[0]+" or "+o[1]+"?"}const s=o.slice(0,He),a=s.pop();return i+s.join(", ")+", or "+a+"?"}function We(e){return e}function ze(e,t){const n=Object.create(null);for(const r of e)n[t(r)]=r;return n}function Ge(e,t,n){const r=Object.create(null);for(const i of e)r[t(i)]=n(i);return r}function Ke(e,t){const n=Object.create(null);for(const r of Object.keys(e))n[r]=t(e[r],r);return n}function Ye(e,t){let n=0,r=0;for(;n0);let a=0;do{++r,a=10*a+o-Qe,o=t.charCodeAt(r)}while(Je(o)&&a>0);if(sa)return 1}else{if(io)return 1;++n,++r}}return e.length-t.length}const Qe=48,Xe=57;function Je(e){return!isNaN(e)&&Qe<=e&&e<=Xe}function Ze(e,t){const n=Object.create(null),r=new et(e),i=Math.floor(.4*e.length)+1;for(const o of t){const e=r.measure(o,i);void 0!==e&&(n[o]=e)}return Object.keys(n).sort(((e,t)=>{const r=n[e]-n[t];return 0!==r?r:Ye(e,t)}))}class et{constructor(e){this._input=e,this._inputLowerCase=e.toLowerCase(),this._inputArray=tt(this._inputLowerCase),this._rows=[new Array(e.length+1).fill(0),new Array(e.length+1).fill(0),new Array(e.length+1).fill(0)]}measure(e,t){if(this._input===e)return 0;const n=e.toLowerCase();if(this._inputLowerCase===n)return 1;let r=tt(n),i=this._inputArray;if(r.lengtht)return;const a=this._rows;for(let c=0;c<=s;c++)a[0][c]=c;for(let c=1;c<=o;c++){const e=a[(c-1)%3],n=a[c%3];let o=n[0]=c;for(let t=1;t<=s;t++){const s=r[c-1]===i[t-1]?0:1;let l=Math.min(e[t]+1,n[t-1]+1,e[t-1]+s);if(c>1&&t>1&&r[c-1]===i[t-2]&&r[c-2]===i[t-1]){const e=a[(c-2)%3][t-2];l=Math.min(l,e+1)}lt)return}const l=a[o%3][s];return l<=t?l:void 0}}function tt(e){const t=e.length,n=new Array(t);for(let r=0;re.value},Variable:{leave:e=>"$"+e.name},Document:{leave:e=>ft(e.definitions,"\n\n")},OperationDefinition:{leave(e){const t=ht("(",ft(e.variableDefinitions,", "),")"),n=ft([e.operation,ft([e.name,t]),ft(e.directives," ")]," ");return("query"===n?"":n+" ")+e.selectionSet}},VariableDefinition:{leave:({variable:e,type:t,defaultValue:n,directives:r})=>e+": "+t+ht(" = ",n)+ht(" ",ft(r," "))},SelectionSet:{leave:({selections:e})=>pt(e)},Field:{leave({alias:e,name:t,arguments:n,directives:r,selectionSet:i}){const o=ht("",e,": ")+t;let s=o+ht("(",ft(n,", "),")");return s.length>80&&(s=o+ht("(\n",mt(ft(n,"\n")),"\n)")),ft([s,ft(r," "),i]," ")}},Argument:{leave:({name:e,value:t})=>e+": "+t},FragmentSpread:{leave:({name:e,directives:t})=>"..."+e+ht(" ",ft(t," "))},InlineFragment:{leave:({typeCondition:e,directives:t,selectionSet:n})=>ft(["...",ht("on ",e),ft(t," "),n]," ")},FragmentDefinition:{leave:({name:e,typeCondition:t,variableDefinitions:n,directives:r,selectionSet:i})=>`fragment ${e}${ht("(",ft(n,", "),")")} on ${t} ${ht("",ft(r," ")," ")}`+i},IntValue:{leave:({value:e})=>e},FloatValue:{leave:({value:e})=>e},StringValue:{leave:({value:e,block:t})=>t?ue(e):`"${e.replace(rt,it)}"`},BooleanValue:{leave:({value:e})=>e?"true":"false"},NullValue:{leave:()=>"null"},EnumValue:{leave:({value:e})=>e},ListValue:{leave:({values:e})=>"["+ft(e,", ")+"]"},ObjectValue:{leave:({fields:e})=>"{"+ft(e,", ")+"}"},ObjectField:{leave:({name:e,value:t})=>e+": "+t},Directive:{leave:({name:e,arguments:t})=>"@"+e+ht("(",ft(t,", "),")")},NamedType:{leave:({name:e})=>e},ListType:{leave:({type:e})=>"["+e+"]"},NonNullType:{leave:({type:e})=>e+"!"},SchemaDefinition:{leave:({description:e,directives:t,operationTypes:n})=>ht("",e,"\n")+ft(["schema",ft(t," "),pt(n)]," ")},OperationTypeDefinition:{leave:({operation:e,type:t})=>e+": "+t},ScalarTypeDefinition:{leave:({description:e,name:t,directives:n})=>ht("",e,"\n")+ft(["scalar",t,ft(n," ")]," ")},ObjectTypeDefinition:{leave:({description:e,name:t,interfaces:n,directives:r,fields:i})=>ht("",e,"\n")+ft(["type",t,ht("implements ",ft(n," & ")),ft(r," "),pt(i)]," ")},FieldDefinition:{leave:({description:e,name:t,arguments:n,type:r,directives:i})=>ht("",e,"\n")+t+(gt(n)?ht("(\n",mt(ft(n,"\n")),"\n)"):ht("(",ft(n,", "),")"))+": "+r+ht(" ",ft(i," "))},InputValueDefinition:{leave:({description:e,name:t,type:n,defaultValue:r,directives:i})=>ht("",e,"\n")+ft([t+": "+n,ht("= ",r),ft(i," ")]," ")},InterfaceTypeDefinition:{leave:({description:e,name:t,interfaces:n,directives:r,fields:i})=>ht("",e,"\n")+ft(["interface",t,ht("implements ",ft(n," & ")),ft(r," "),pt(i)]," ")},UnionTypeDefinition:{leave:({description:e,name:t,directives:n,types:r})=>ht("",e,"\n")+ft(["union",t,ft(n," "),ht("= ",ft(r," | "))]," ")},EnumTypeDefinition:{leave:({description:e,name:t,directives:n,values:r})=>ht("",e,"\n")+ft(["enum",t,ft(n," "),pt(r)]," ")},EnumValueDefinition:{leave:({description:e,name:t,directives:n})=>ht("",e,"\n")+ft([t,ft(n," ")]," ")},InputObjectTypeDefinition:{leave:({description:e,name:t,directives:n,fields:r})=>ht("",e,"\n")+ft(["input",t,ft(n," "),pt(r)]," ")},DirectiveDefinition:{leave:({description:e,name:t,arguments:n,repeatable:r,locations:i})=>ht("",e,"\n")+"directive @"+t+(gt(n)?ht("(\n",mt(ft(n,"\n")),"\n)"):ht("(",ft(n,", "),")"))+(r?" repeatable":"")+" on "+ft(i," | ")},SchemaExtension:{leave:({directives:e,operationTypes:t})=>ft(["extend schema",ft(e," "),pt(t)]," ")},ScalarTypeExtension:{leave:({name:e,directives:t})=>ft(["extend scalar",e,ft(t," ")]," ")},ObjectTypeExtension:{leave:({name:e,interfaces:t,directives:n,fields:r})=>ft(["extend type",e,ht("implements ",ft(t," & ")),ft(n," "),pt(r)]," ")},InterfaceTypeExtension:{leave:({name:e,interfaces:t,directives:n,fields:r})=>ft(["extend interface",e,ht("implements ",ft(t," & ")),ft(n," "),pt(r)]," ")},UnionTypeExtension:{leave:({name:e,directives:t,types:n})=>ft(["extend union",e,ft(t," "),ht("= ",ft(n," | "))]," ")},EnumTypeExtension:{leave:({name:e,directives:t,values:n})=>ft(["extend enum",e,ft(t," "),pt(n)]," ")},InputObjectTypeExtension:{leave:({name:e,directives:t,fields:n})=>ft(["extend input",e,ft(t," "),pt(n)]," ")}};function ft(e,t=""){var n;return null!==(n=null==e?void 0:e.filter((e=>e)).join(t))&&void 0!==n?n:""}function pt(e){return ht("{\n",mt(ft(e,"\n")),"\n}")}function ht(e,t,n=""){return null!=t&&""!==t?e+t+n:""}function mt(e){return ht(" ",e.replace(/\n/g,"\n "))}function gt(e){var t;return null!==(t=null==e?void 0:e.some((e=>e.includes("\n"))))&&void 0!==t&&t}function vt(e,t){switch(e.kind){case J.NULL:return null;case J.INT:return parseInt(e.value,10);case J.FLOAT:return parseFloat(e.value);case J.STRING:case J.ENUM:case J.BOOLEAN:return e.value;case J.LIST:return e.values.map((e=>vt(e,t)));case J.OBJECT:return Ge(e.fields,(e=>e.name.value),(e=>vt(e.value,t)));case J.VARIABLE:return null==t?void 0:t[e.name.value]}}function yt(e){if(null!=e||I(!1,"Must provide name."),"string"==typeof e||I(!1,"Expected name to be a string."),0===e.length)throw new B("Expected name to be a non-empty string.");for(let t=1;to(vt(e,t)),this.extensions=nt(e.extensions),this.astNode=e.astNode,this.extensionASTNodes=null!==(i=e.extensionASTNodes)&&void 0!==i?i:[],null==e.specifiedByURL||"string"==typeof e.specifiedByURL||I(!1,`${this.name} must provide "specifiedByURL" as a string, but got: ${Le(e.specifiedByURL)}.`),null==e.serialize||"function"==typeof e.serialize||I(!1,`${this.name} must provide "serialize" function. If this custom Scalar is also used as an input type, ensure "parseValue" and "parseLiteral" functions are also provided.`),e.parseLiteral&&("function"==typeof e.parseValue&&"function"==typeof e.parseLiteral||I(!1,`${this.name} must provide both "parseValue" and "parseLiteral" functions.`))}get[Symbol.toStringTag](){return"GraphQLScalarType"}toConfig(){return{name:this.name,description:this.description,specifiedByURL:this.specifiedByURL,serialize:this.serialize,parseValue:this.parseValue,parseLiteral:this.parseLiteral,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:this.extensionASTNodes}}toString(){return this.name}toJSON(){return this.toString()}}class Kt{constructor(e){var t;this.name=yt(e.name),this.description=e.description,this.isTypeOf=e.isTypeOf,this.extensions=nt(e.extensions),this.astNode=e.astNode,this.extensionASTNodes=null!==(t=e.extensionASTNodes)&&void 0!==t?t:[],this._fields=()=>Qt(e),this._interfaces=()=>Yt(e),null==e.isTypeOf||"function"==typeof e.isTypeOf||I(!1,`${this.name} must provide "isTypeOf" as a function, but got: ${Le(e.isTypeOf)}.`)}get[Symbol.toStringTag](){return"GraphQLObjectType"}getFields(){return"function"==typeof this._fields&&(this._fields=this._fields()),this._fields}getInterfaces(){return"function"==typeof this._interfaces&&(this._interfaces=this._interfaces()),this._interfaces}toConfig(){return{name:this.name,description:this.description,interfaces:this.getInterfaces(),fields:Zt(this.getFields()),isTypeOf:this.isTypeOf,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:this.extensionASTNodes}}toString(){return this.name}toJSON(){return this.toString()}}function Yt(e){var t;const n=Wt(null!==(t=e.interfaces)&&void 0!==t?t:[]);return Array.isArray(n)||I(!1,`${e.name} interfaces must be an Array or a function which returns an Array.`),n}function Qt(e){const t=zt(e.fields);return Jt(t)||I(!1,`${e.name} fields must be an object with field names as keys or a function which returns such an object.`),Ke(t,((t,n)=>{var r;Jt(t)||I(!1,`${e.name}.${n} field config must be an object.`),null==t.resolve||"function"==typeof t.resolve||I(!1,`${e.name}.${n} field resolver must be a function if provided, but got: ${Le(t.resolve)}.`);const i=null!==(r=t.args)&&void 0!==r?r:{};return Jt(i)||I(!1,`${e.name}.${n} args must be an object with argument names as keys.`),{name:yt(n),description:t.description,type:t.type,args:Xt(i),resolve:t.resolve,subscribe:t.subscribe,deprecationReason:t.deprecationReason,extensions:nt(t.extensions),astNode:t.astNode}}))}function Xt(e){return Object.entries(e).map((([e,t])=>({name:yt(e),description:t.description,type:t.type,defaultValue:t.defaultValue,deprecationReason:t.deprecationReason,extensions:nt(t.extensions),astNode:t.astNode})))}function Jt(e){return L(e)&&!Array.isArray(e)}function Zt(e){return Ke(e,(e=>({description:e.description,type:e.type,args:en(e.args),resolve:e.resolve,subscribe:e.subscribe,deprecationReason:e.deprecationReason,extensions:e.extensions,astNode:e.astNode})))}function en(e){return Ge(e,(e=>e.name),(e=>({description:e.description,type:e.type,defaultValue:e.defaultValue,deprecationReason:e.deprecationReason,extensions:e.extensions,astNode:e.astNode})))}function tn(e){return At(e.type)&&void 0===e.defaultValue}class nn{constructor(e){var t;this.name=yt(e.name),this.description=e.description,this.resolveType=e.resolveType,this.extensions=nt(e.extensions),this.astNode=e.astNode,this.extensionASTNodes=null!==(t=e.extensionASTNodes)&&void 0!==t?t:[],this._fields=Qt.bind(void 0,e),this._interfaces=Yt.bind(void 0,e),null==e.resolveType||"function"==typeof e.resolveType||I(!1,`${this.name} must provide "resolveType" as a function, but got: ${Le(e.resolveType)}.`)}get[Symbol.toStringTag](){return"GraphQLInterfaceType"}getFields(){return"function"==typeof this._fields&&(this._fields=this._fields()),this._fields}getInterfaces(){return"function"==typeof this._interfaces&&(this._interfaces=this._interfaces()),this._interfaces}toConfig(){return{name:this.name,description:this.description,interfaces:this.getInterfaces(),fields:Zt(this.getFields()),resolveType:this.resolveType,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:this.extensionASTNodes}}toString(){return this.name}toJSON(){return this.toString()}}class rn{constructor(e){var t;this.name=yt(e.name),this.description=e.description,this.resolveType=e.resolveType,this.extensions=nt(e.extensions),this.astNode=e.astNode,this.extensionASTNodes=null!==(t=e.extensionASTNodes)&&void 0!==t?t:[],this._types=on.bind(void 0,e),null==e.resolveType||"function"==typeof e.resolveType||I(!1,`${this.name} must provide "resolveType" as a function, but got: ${Le(e.resolveType)}.`)}get[Symbol.toStringTag](){return"GraphQLUnionType"}getTypes(){return"function"==typeof this._types&&(this._types=this._types()),this._types}toConfig(){return{name:this.name,description:this.description,types:this.getTypes(),resolveType:this.resolveType,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:this.extensionASTNodes}}toString(){return this.name}toJSON(){return this.toString()}}function on(e){const t=Wt(e.types);return Array.isArray(t)||I(!1,`Must provide Array of types or a function which returns such an array for Union ${e.name}.`),t}class sn{constructor(e){var t;this.name=yt(e.name),this.description=e.description,this.extensions=nt(e.extensions),this.astNode=e.astNode,this.extensionASTNodes=null!==(t=e.extensionASTNodes)&&void 0!==t?t:[],this._values="function"==typeof e.values?e.values:ln(this.name,e.values),this._valueLookup=null,this._nameLookup=null}get[Symbol.toStringTag](){return"GraphQLEnumType"}getValues(){return"function"==typeof this._values&&(this._values=ln(this.name,this._values())),this._values}getValue(e){return null===this._nameLookup&&(this._nameLookup=ze(this.getValues(),(e=>e.name))),this._nameLookup[e]}serialize(e){null===this._valueLookup&&(this._valueLookup=new Map(this.getValues().map((e=>[e.value,e]))));const t=this._valueLookup.get(e);if(void 0===t)throw new B(`Enum "${this.name}" cannot represent value: ${Le(e)}`);return t.name}parseValue(e){if("string"!=typeof e){const t=Le(e);throw new B(`Enum "${this.name}" cannot represent non-string value: ${t}.`+an(this,t))}const t=this.getValue(e);if(null==t)throw new B(`Value "${e}" does not exist in "${this.name}" enum.`+an(this,e));return t.value}parseLiteral(e,t){if(e.kind!==J.ENUM){const t=ut(e);throw new B(`Enum "${this.name}" cannot represent non-enum value: ${t}.`+an(this,t),{nodes:e})}const n=this.getValue(e.value);if(null==n){const t=ut(e);throw new B(`Value "${t}" does not exist in "${this.name}" enum.`+an(this,t),{nodes:e})}return n.value}toConfig(){const e=Ge(this.getValues(),(e=>e.name),(e=>({description:e.description,value:e.value,deprecationReason:e.deprecationReason,extensions:e.extensions,astNode:e.astNode})));return{name:this.name,description:this.description,values:e,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:this.extensionASTNodes}}toString(){return this.name}toJSON(){return this.toString()}}function an(e,t){return qe("the enum value",Ze(t,e.getValues().map((e=>e.name))))}function ln(e,t){return Jt(t)||I(!1,`${e} values must be an object with value names as keys.`),Object.entries(t).map((([t,n])=>(Jt(n)||I(!1,`${e}.${t} must refer to an object with a "value" key representing an internal value but got: ${Le(n)}.`),{name:bt(t),description:n.description,value:void 0!==n.value?n.value:t,deprecationReason:n.deprecationReason,extensions:nt(n.extensions),astNode:n.astNode})))}class cn{constructor(e){var t,n;this.name=yt(e.name),this.description=e.description,this.extensions=nt(e.extensions),this.astNode=e.astNode,this.extensionASTNodes=null!==(t=e.extensionASTNodes)&&void 0!==t?t:[],this.isOneOf=null!==(n=e.isOneOf)&&void 0!==n&&n,this._fields=un.bind(void 0,e)}get[Symbol.toStringTag](){return"GraphQLInputObjectType"}getFields(){return"function"==typeof this._fields&&(this._fields=this._fields()),this._fields}toConfig(){const e=Ke(this.getFields(),(e=>({description:e.description,type:e.type,defaultValue:e.defaultValue,deprecationReason:e.deprecationReason,extensions:e.extensions,astNode:e.astNode})));return{name:this.name,description:this.description,fields:e,extensions:this.extensions,astNode:this.astNode,extensionASTNodes:this.extensionASTNodes,isOneOf:this.isOneOf}}toString(){return this.name}toJSON(){return this.toString()}}function un(e){const t=zt(e.fields);return Jt(t)||I(!1,`${e.name} fields must be an object with field names as keys or a function which returns such an object.`),Ke(t,((t,n)=>(!("resolve"in t)||I(!1,`${e.name}.${n} field has a resolve property, but Input Types cannot define resolvers.`),{name:yt(n),description:t.description,type:t.type,defaultValue:t.defaultValue,deprecationReason:t.deprecationReason,extensions:nt(t.extensions),astNode:t.astNode})))}function dn(e){return At(e.type)&&void 0===e.defaultValue}function fn(e,t){return e===t||(At(e)&&At(t)||!(!Dt(e)||!Dt(t)))&&fn(e.ofType,t.ofType)}function pn(e,t,n){return t===n||(At(n)?!!At(t)&&pn(e,t.ofType,n.ofType):At(t)?pn(e,t.ofType,n):Dt(n)?!!Dt(t)&&pn(e,t.ofType,n.ofType):!Dt(t)&&(Rt(n)&&(Ct(t)||wt(t))&&e.isSubType(n,t)))}function hn(e,t,n){return t===n||(Rt(t)?Rt(n)?e.getPossibleTypes(t).some((t=>e.isSubType(n,t))):e.isSubType(t,n):!!Rt(n)&&e.isSubType(n,t))}const mn=2147483647,gn=-2147483648,vn=new Gt({name:"Int",description:"The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.",serialize(e){const t=Cn(e);if("boolean"==typeof t)return t?1:0;let n=t;if("string"==typeof t&&""!==t&&(n=Number(t)),"number"!=typeof n||!Number.isInteger(n))throw new B(`Int cannot represent non-integer value: ${Le(t)}`);if(n>mn||nmn||emn||te.name===t))}function Cn(e){if(L(e)){if("function"==typeof e.valueOf){const t=e.valueOf();if(!L(t))return t}if("function"==typeof e.toJSON)return e.toJSON()}return e}function Sn(e){return Re(e,kn)}class kn{constructor(e){var t,n;this.name=yt(e.name),this.description=e.description,this.locations=e.locations,this.isRepeatable=null!==(t=e.isRepeatable)&&void 0!==t&&t,this.extensions=nt(e.extensions),this.astNode=e.astNode,Array.isArray(e.locations)||I(!1,`@${e.name} locations must be an Array.`);const r=null!==(n=e.args)&&void 0!==n?n:{};L(r)&&!Array.isArray(r)||I(!1,`@${e.name} args must be an object with argument names as keys.`),this.args=Xt(r)}get[Symbol.toStringTag](){return"GraphQLDirective"}toConfig(){return{name:this.name,description:this.description,locations:this.locations,args:en(this.args),isRepeatable:this.isRepeatable,extensions:this.extensions,astNode:this.astNode}}toString(){return"@"+this.name}toJSON(){return this.toString()}}const _n=new kn({name:"include",description:"Directs the executor to include this field or fragment only when the `if` argument is true.",locations:[Q.FIELD,Q.FRAGMENT_SPREAD,Q.INLINE_FRAGMENT],args:{if:{type:new jt(En),description:"Included when true."}}}),Nn=new kn({name:"skip",description:"Directs the executor to skip this field or fragment when the `if` argument is true.",locations:[Q.FIELD,Q.FRAGMENT_SPREAD,Q.INLINE_FRAGMENT],args:{if:{type:new jt(En),description:"Skipped when true."}}}),Dn="No longer supported",An=new kn({name:"deprecated",description:"Marks an element of a GraphQL schema as no longer supported.",locations:[Q.FIELD_DEFINITION,Q.ARGUMENT_DEFINITION,Q.INPUT_FIELD_DEFINITION,Q.ENUM_VALUE],args:{reason:{type:bn,description:"Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).",defaultValue:Dn}}}),In=new kn({name:"specifiedBy",description:"Exposes a URL that specifies the behavior of this scalar.",locations:[Q.SCALAR],args:{url:{type:new jt(bn),description:"The URL that specifies the behavior of this scalar."}}}),On=new kn({name:"oneOf",description:"Indicates exactly one field must be supplied and this field must not be `null`.",locations:[Q.INPUT_OBJECT],args:{}}),Ln=Object.freeze([_n,Nn,An,In,On]);function Mn(e){return Ln.some((({name:t})=>t===e.name))}function Rn(e){return"object"==typeof e&&"function"==typeof(null==e?void 0:e[Symbol.iterator])}function Fn(e,t){if(At(t)){const n=Fn(e,t.ofType);return(null==n?void 0:n.kind)===J.NULL?null:n}if(null===e)return{kind:J.NULL};if(void 0===e)return null;if(Dt(t)){const n=t.ofType;if(Rn(e)){const t=[];for(const r of e){const e=Fn(r,n);null!=e&&t.push(e)}return{kind:J.LIST,values:t}}return Fn(e,n)}if(Nt(t)){if(!L(e))return null;const n=[];for(const r of Object.values(t.getFields())){const t=Fn(e[r.name],r.type);t&&n.push({kind:J.OBJECT_FIELD,name:{kind:J.NAME,value:r.name},value:t})}return{kind:J.OBJECT,fields:n}}if(Lt(t)){const n=t.serialize(e);if(null==n)return null;if("boolean"==typeof n)return{kind:J.BOOLEAN,value:n};if("number"==typeof n&&Number.isFinite(n)){const e=String(n);return Pn.test(e)?{kind:J.INT,value:e}:{kind:J.FLOAT,value:e}}if("string"==typeof n)return _t(t)?{kind:J.ENUM,value:n}:t===xn&&Pn.test(n)?{kind:J.INT,value:n}:{kind:J.STRING,value:n};throw new TypeError(`Cannot convert value to AST: ${Le(n)}.`)}M(!1,"Unexpected input type: "+Le(t))}const Pn=/^-?(?:0|[1-9][0-9]*)$/,jn=new Kt({name:"__Schema",description:"A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.",fields:()=>({description:{type:bn,resolve:e=>e.description},types:{description:"A list of all types supported by this server.",type:new jt(new Pt(new jt($n))),resolve:e=>Object.values(e.getTypeMap())},queryType:{description:"The type that query operations will be rooted at.",type:new jt($n),resolve:e=>e.getQueryType()},mutationType:{description:"If this server supports mutation, the type that mutation operations will be rooted at.",type:$n,resolve:e=>e.getMutationType()},subscriptionType:{description:"If this server support subscription, the type that subscription operations will be rooted at.",type:$n,resolve:e=>e.getSubscriptionType()},directives:{description:"A list of all directives supported by this server.",type:new jt(new Pt(new jt(Vn))),resolve:e=>e.getDirectives()}})}),Vn=new Kt({name:"__Directive",description:"A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.",fields:()=>({name:{type:new jt(bn),resolve:e=>e.name},description:{type:bn,resolve:e=>e.description},isRepeatable:{type:new jt(En),resolve:e=>e.isRepeatable},locations:{type:new jt(new Pt(new jt(Bn))),resolve:e=>e.locations},args:{type:new jt(new Pt(new jt(Hn))),args:{includeDeprecated:{type:En,defaultValue:!1}},resolve:(e,{includeDeprecated:t})=>t?e.args:e.args.filter((e=>null==e.deprecationReason))}})}),Bn=new sn({name:"__DirectiveLocation",description:"A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.",values:{QUERY:{value:Q.QUERY,description:"Location adjacent to a query operation."},MUTATION:{value:Q.MUTATION,description:"Location adjacent to a mutation operation."},SUBSCRIPTION:{value:Q.SUBSCRIPTION,description:"Location adjacent to a subscription operation."},FIELD:{value:Q.FIELD,description:"Location adjacent to a field."},FRAGMENT_DEFINITION:{value:Q.FRAGMENT_DEFINITION,description:"Location adjacent to a fragment definition."},FRAGMENT_SPREAD:{value:Q.FRAGMENT_SPREAD,description:"Location adjacent to a fragment spread."},INLINE_FRAGMENT:{value:Q.INLINE_FRAGMENT,description:"Location adjacent to an inline fragment."},VARIABLE_DEFINITION:{value:Q.VARIABLE_DEFINITION,description:"Location adjacent to a variable definition."},SCHEMA:{value:Q.SCHEMA,description:"Location adjacent to a schema definition."},SCALAR:{value:Q.SCALAR,description:"Location adjacent to a scalar definition."},OBJECT:{value:Q.OBJECT,description:"Location adjacent to an object type definition."},FIELD_DEFINITION:{value:Q.FIELD_DEFINITION,description:"Location adjacent to a field definition."},ARGUMENT_DEFINITION:{value:Q.ARGUMENT_DEFINITION,description:"Location adjacent to an argument definition."},INTERFACE:{value:Q.INTERFACE,description:"Location adjacent to an interface definition."},UNION:{value:Q.UNION,description:"Location adjacent to a union definition."},ENUM:{value:Q.ENUM,description:"Location adjacent to an enum definition."},ENUM_VALUE:{value:Q.ENUM_VALUE,description:"Location adjacent to an enum value definition."},INPUT_OBJECT:{value:Q.INPUT_OBJECT,description:"Location adjacent to an input object type definition."},INPUT_FIELD_DEFINITION:{value:Q.INPUT_FIELD_DEFINITION,description:"Location adjacent to an input object field definition."}}}),$n=new Kt({name:"__Type",description:"The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByURL`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.",fields:()=>({kind:{type:new jt(Gn),resolve:e=>xt(e)?Wn.SCALAR:wt(e)?Wn.OBJECT:Ct(e)?Wn.INTERFACE:kt(e)?Wn.UNION:_t(e)?Wn.ENUM:Nt(e)?Wn.INPUT_OBJECT:Dt(e)?Wn.LIST:At(e)?Wn.NON_NULL:void M(!1,`Unexpected type: "${Le(e)}".`)},name:{type:bn,resolve:e=>"name"in e?e.name:void 0},description:{type:bn,resolve:e=>"description"in e?e.description:void 0},specifiedByURL:{type:bn,resolve:e=>"specifiedByURL"in e?e.specifiedByURL:void 0},fields:{type:new Pt(new jt(Un)),args:{includeDeprecated:{type:En,defaultValue:!1}},resolve(e,{includeDeprecated:t}){if(wt(e)||Ct(e)){const n=Object.values(e.getFields());return t?n:n.filter((e=>null==e.deprecationReason))}}},interfaces:{type:new Pt(new jt($n)),resolve(e){if(wt(e)||Ct(e))return e.getInterfaces()}},possibleTypes:{type:new Pt(new jt($n)),resolve(e,t,n,{schema:r}){if(Rt(e))return r.getPossibleTypes(e)}},enumValues:{type:new Pt(new jt(qn)),args:{includeDeprecated:{type:En,defaultValue:!1}},resolve(e,{includeDeprecated:t}){if(_t(e)){const n=e.getValues();return t?n:n.filter((e=>null==e.deprecationReason))}}},inputFields:{type:new Pt(new jt(Hn)),args:{includeDeprecated:{type:En,defaultValue:!1}},resolve(e,{includeDeprecated:t}){if(Nt(e)){const n=Object.values(e.getFields());return t?n:n.filter((e=>null==e.deprecationReason))}}},ofType:{type:$n,resolve:e=>"ofType"in e?e.ofType:void 0},isOneOf:{type:En,resolve:e=>{if(Nt(e))return e.isOneOf}}})}),Un=new Kt({name:"__Field",description:"Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.",fields:()=>({name:{type:new jt(bn),resolve:e=>e.name},description:{type:bn,resolve:e=>e.description},args:{type:new jt(new Pt(new jt(Hn))),args:{includeDeprecated:{type:En,defaultValue:!1}},resolve:(e,{includeDeprecated:t})=>t?e.args:e.args.filter((e=>null==e.deprecationReason))},type:{type:new jt($n),resolve:e=>e.type},isDeprecated:{type:new jt(En),resolve:e=>null!=e.deprecationReason},deprecationReason:{type:bn,resolve:e=>e.deprecationReason}})}),Hn=new Kt({name:"__InputValue",description:"Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.",fields:()=>({name:{type:new jt(bn),resolve:e=>e.name},description:{type:bn,resolve:e=>e.description},type:{type:new jt($n),resolve:e=>e.type},defaultValue:{type:bn,description:"A GraphQL-formatted string representing the default value for this input value.",resolve(e){const{type:t,defaultValue:n}=e,r=Fn(n,t);return r?ut(r):null}},isDeprecated:{type:new jt(En),resolve:e=>null!=e.deprecationReason},deprecationReason:{type:bn,resolve:e=>e.deprecationReason}})}),qn=new Kt({name:"__EnumValue",description:"One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.",fields:()=>({name:{type:new jt(bn),resolve:e=>e.name},description:{type:bn,resolve:e=>e.description},isDeprecated:{type:new jt(En),resolve:e=>null!=e.deprecationReason},deprecationReason:{type:bn,resolve:e=>e.deprecationReason}})});var Wn,zn;(zn=Wn||(Wn={})).SCALAR="SCALAR",zn.OBJECT="OBJECT",zn.INTERFACE="INTERFACE",zn.UNION="UNION",zn.ENUM="ENUM",zn.INPUT_OBJECT="INPUT_OBJECT",zn.LIST="LIST",zn.NON_NULL="NON_NULL";const Gn=new sn({name:"__TypeKind",description:"An enum describing what kind of type a given `__Type` is.",values:{SCALAR:{value:Wn.SCALAR,description:"Indicates this type is a scalar."},OBJECT:{value:Wn.OBJECT,description:"Indicates this type is an object. `fields` and `interfaces` are valid fields."},INTERFACE:{value:Wn.INTERFACE,description:"Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields."},UNION:{value:Wn.UNION,description:"Indicates this type is a union. `possibleTypes` is a valid field."},ENUM:{value:Wn.ENUM,description:"Indicates this type is an enum. `enumValues` is a valid field."},INPUT_OBJECT:{value:Wn.INPUT_OBJECT,description:"Indicates this type is an input object. `inputFields` is a valid field."},LIST:{value:Wn.LIST,description:"Indicates this type is a list. `ofType` is a valid field."},NON_NULL:{value:Wn.NON_NULL,description:"Indicates this type is a non-null. `ofType` is a valid field."}}}),Kn={name:"__schema",type:new jt(jn),description:"Access the current type schema of this server.",args:[],resolve:(e,t,n,{schema:r})=>r,deprecationReason:void 0,extensions:Object.create(null),astNode:void 0},Yn={name:"__type",type:$n,description:"Request the type information of a single type.",args:[{name:"name",description:void 0,type:new jt(bn),defaultValue:void 0,deprecationReason:void 0,extensions:Object.create(null),astNode:void 0}],resolve:(e,{name:t},n,{schema:r})=>r.getType(t),deprecationReason:void 0,extensions:Object.create(null),astNode:void 0},Qn={name:"__typename",type:new jt(bn),description:"The name of the current Object type at runtime.",args:[],resolve:(e,t,n,{parentType:r})=>r.name,deprecationReason:void 0,extensions:Object.create(null),astNode:void 0},Xn=Object.freeze([jn,Vn,Bn,$n,Un,Hn,qn,Gn]);function Jn(e){return Xn.some((({name:t})=>e.name===t))}function Zn(e){return Re(e,tr)}function er(e){if(!Zn(e))throw new Error(`Expected ${Le(e)} to be a GraphQL schema.`);return e}class tr{constructor(e){var t,n;this.__validationErrors=!0===e.assumeValid?[]:void 0,L(e)||I(!1,"Must provide configuration object."),!e.types||Array.isArray(e.types)||I(!1,`"types" must be Array if provided but got: ${Le(e.types)}.`),!e.directives||Array.isArray(e.directives)||I(!1,`"directives" must be Array if provided but got: ${Le(e.directives)}.`),this.description=e.description,this.extensions=nt(e.extensions),this.astNode=e.astNode,this.extensionASTNodes=null!==(t=e.extensionASTNodes)&&void 0!==t?t:[],this._queryType=e.query,this._mutationType=e.mutation,this._subscriptionType=e.subscription,this._directives=null!==(n=e.directives)&&void 0!==n?n:Ln;const r=new Set(e.types);if(null!=e.types)for(const i of e.types)r.delete(i),nr(i,r);null!=this._queryType&&nr(this._queryType,r),null!=this._mutationType&&nr(this._mutationType,r),null!=this._subscriptionType&&nr(this._subscriptionType,r);for(const i of this._directives)if(Sn(i))for(const e of i.args)nr(e.type,r);nr(jn,r),this._typeMap=Object.create(null),this._subTypeMap=Object.create(null),this._implementationsMap=Object.create(null);for(const i of r){if(null==i)continue;const e=i.name;if(e||I(!1,"One of the provided types for building the Schema is missing a name."),void 0!==this._typeMap[e])throw new Error(`Schema must contain uniquely named types but contains multiple types named "${e}".`);if(this._typeMap[e]=i,Ct(i)){for(const t of i.getInterfaces())if(Ct(t)){let e=this._implementationsMap[t.name];void 0===e&&(e=this._implementationsMap[t.name]={objects:[],interfaces:[]}),e.interfaces.push(i)}}else if(wt(i))for(const t of i.getInterfaces())if(Ct(t)){let e=this._implementationsMap[t.name];void 0===e&&(e=this._implementationsMap[t.name]={objects:[],interfaces:[]}),e.objects.push(i)}}}get[Symbol.toStringTag](){return"GraphQLSchema"}getQueryType(){return this._queryType}getMutationType(){return this._mutationType}getSubscriptionType(){return this._subscriptionType}getRootType(e){switch(e){case K.QUERY:return this.getQueryType();case K.MUTATION:return this.getMutationType();case K.SUBSCRIPTION:return this.getSubscriptionType()}}getTypeMap(){return this._typeMap}getType(e){return this.getTypeMap()[e]}getPossibleTypes(e){return kt(e)?e.getTypes():this.getImplementations(e).objects}getImplementations(e){const t=this._implementationsMap[e.name];return null!=t?t:{objects:[],interfaces:[]}}isSubType(e,t){let n=this._subTypeMap[e.name];if(void 0===n){if(n=Object.create(null),kt(e))for(const t of e.getTypes())n[t.name]=!0;else{const t=this.getImplementations(e);for(const e of t.objects)n[e.name]=!0;for(const e of t.interfaces)n[e.name]=!0}this._subTypeMap[e.name]=n}return void 0!==n[t.name]}getDirectives(){return this._directives}getDirective(e){return this.getDirectives().find((t=>t.name===e))}toConfig(){return{description:this.description,query:this.getQueryType(),mutation:this.getMutationType(),subscription:this.getSubscriptionType(),types:Object.values(this.getTypeMap()),directives:this.getDirectives(),extensions:this.extensions,astNode:this.astNode,extensionASTNodes:this.extensionASTNodes,assumeValid:void 0!==this.__validationErrors}}}function nr(e,t){const n=qt(e);if(!t.has(n))if(t.add(n),kt(n))for(const r of n.getTypes())nr(r,t);else if(wt(n)||Ct(n)){for(const e of n.getInterfaces())nr(e,t);for(const e of Object.values(n.getFields())){nr(e.type,t);for(const n of e.args)nr(n.type,t)}}else if(Nt(n))for(const r of Object.values(n.getFields()))nr(r.type,t);return t}function rr(e){if(er(e),e.__validationErrors)return e.__validationErrors;const t=new or(e);!function(e){const t=e.schema,n=t.getQueryType();if(n){if(!wt(n)){var r;e.reportError(`Query root type must be Object type, it cannot be ${Le(n)}.`,null!==(r=sr(t,K.QUERY))&&void 0!==r?r:n.astNode)}}else e.reportError("Query root type must be provided.",t.astNode);const i=t.getMutationType();var o;i&&!wt(i)&&e.reportError(`Mutation root type must be Object type if provided, it cannot be ${Le(i)}.`,null!==(o=sr(t,K.MUTATION))&&void 0!==o?o:i.astNode);const s=t.getSubscriptionType();var a;s&&!wt(s)&&e.reportError(`Subscription root type must be Object type if provided, it cannot be ${Le(s)}.`,null!==(a=sr(t,K.SUBSCRIPTION))&&void 0!==a?a:s.astNode)}(t),function(e){for(const n of e.schema.getDirectives())if(Sn(n)){ar(e,n),0===n.locations.length&&e.reportError(`Directive @${n.name} must include 1 or more locations.`,n.astNode);for(const r of n.args){var t;if(ar(e,r),It(r.type)||e.reportError(`The type of @${n.name}(${r.name}:) must be Input Type but got: ${Le(r.type)}.`,r.astNode),tn(r)&&null!=r.deprecationReason)e.reportError(`Required argument @${n.name}(${r.name}:) cannot be deprecated.`,[yr(r.astNode),null===(t=r.astNode)||void 0===t?void 0:t.type])}}else e.reportError(`Expected directive but got: ${Le(n)}.`,null==n?void 0:n.astNode)}(t),function(e){const t=function(e){const t=Object.create(null),n=[],r=Object.create(null);return i;function i(o){if(t[o.name])return;t[o.name]=!0,r[o.name]=n.length;const s=Object.values(o.getFields());for(const t of s)if(At(t.type)&&Nt(t.type.ofType)){const o=t.type.ofType,s=r[o.name];if(n.push(t),void 0===s)i(o);else{const t=n.slice(s),r=t.map((e=>e.name)).join(".");e.reportError(`Cannot reference Input Object "${o.name}" within itself through a series of non-null fields: "${r}".`,t.map((e=>e.astNode)))}n.pop()}r[o.name]=void 0}}(e),n=e.schema.getTypeMap();for(const r of Object.values(n))Ht(r)?(Jn(r)||ar(e,r),wt(r)||Ct(r)?(lr(e,r),cr(e,r)):kt(r)?fr(e,r):_t(r)?pr(e,r):Nt(r)&&(hr(e,r),t(r))):e.reportError(`Expected GraphQL named type but got: ${Le(r)}.`,r.astNode)}(t);const n=t.getErrors();return e.__validationErrors=n,n}function ir(e){const t=rr(e);if(0!==t.length)throw new Error(t.map((e=>e.message)).join("\n\n"))}class or{constructor(e){this._errors=[],this.schema=e}reportError(e,t){const n=Array.isArray(t)?t.filter(Boolean):t;this._errors.push(new B(e,{nodes:n}))}getErrors(){return this._errors}}function sr(e,t){var n;return null===(n=[e.astNode,...e.extensionASTNodes].flatMap((e=>{var t;return null!==(t=null==e?void 0:e.operationTypes)&&void 0!==t?t:[]})).find((e=>e.operation===t)))||void 0===n?void 0:n.type}function ar(e,t){t.name.startsWith("__")&&e.reportError(`Name "${t.name}" must not begin with "__", which is reserved by GraphQL introspection.`,t.astNode)}function lr(e,t){const n=Object.values(t.getFields());0===n.length&&e.reportError(`Type ${t.name} must define one or more fields.`,[t.astNode,...t.extensionASTNodes]);for(const s of n){var r;if(ar(e,s),!Ot(s.type))e.reportError(`The type of ${t.name}.${s.name} must be Output Type but got: ${Le(s.type)}.`,null===(r=s.astNode)||void 0===r?void 0:r.type);for(const n of s.args){const r=n.name;var i,o;if(ar(e,n),!It(n.type))e.reportError(`The type of ${t.name}.${s.name}(${r}:) must be Input Type but got: ${Le(n.type)}.`,null===(i=n.astNode)||void 0===i?void 0:i.type);if(tn(n)&&null!=n.deprecationReason)e.reportError(`Required argument ${t.name}.${s.name}(${r}:) cannot be deprecated.`,[yr(n.astNode),null===(o=n.astNode)||void 0===o?void 0:o.type])}}}function cr(e,t){const n=Object.create(null);for(const r of t.getInterfaces())Ct(r)?t!==r?n[r.name]?e.reportError(`Type ${t.name} can only implement ${r.name} once.`,gr(t,r)):(n[r.name]=!0,dr(e,t,r),ur(e,t,r)):e.reportError(`Type ${t.name} cannot implement itself because it would create a circular reference.`,gr(t,r)):e.reportError(`Type ${Le(t)} must only implement Interface types, it cannot implement ${Le(r)}.`,gr(t,r))}function ur(e,t,n){const r=t.getFields();for(const l of Object.values(n.getFields())){const c=l.name,u=r[c];if(u){var i,o;if(!pn(e.schema,u.type,l.type))e.reportError(`Interface field ${n.name}.${c} expects type ${Le(l.type)} but ${t.name}.${c} is type ${Le(u.type)}.`,[null===(i=l.astNode)||void 0===i?void 0:i.type,null===(o=u.astNode)||void 0===o?void 0:o.type]);for(const r of l.args){const i=r.name,o=u.args.find((e=>e.name===i));var s,a;if(o){if(!fn(r.type,o.type))e.reportError(`Interface field argument ${n.name}.${c}(${i}:) expects type ${Le(r.type)} but ${t.name}.${c}(${i}:) is type ${Le(o.type)}.`,[null===(s=r.astNode)||void 0===s?void 0:s.type,null===(a=o.astNode)||void 0===a?void 0:a.type])}else e.reportError(`Interface field argument ${n.name}.${c}(${i}:) expected but ${t.name}.${c} does not provide it.`,[r.astNode,u.astNode])}for(const r of u.args){const i=r.name;!l.args.find((e=>e.name===i))&&tn(r)&&e.reportError(`Object field ${t.name}.${c} includes required argument ${i} that is missing from the Interface field ${n.name}.${c}.`,[r.astNode,l.astNode])}}else e.reportError(`Interface field ${n.name}.${c} expected but ${t.name} does not provide it.`,[l.astNode,t.astNode,...t.extensionASTNodes])}}function dr(e,t,n){const r=t.getInterfaces();for(const i of n.getInterfaces())r.includes(i)||e.reportError(i===t?`Type ${t.name} cannot implement ${n.name} because it would create a circular reference.`:`Type ${t.name} must implement ${i.name} because it is implemented by ${n.name}.`,[...gr(n,i),...gr(t,n)])}function fr(e,t){const n=t.getTypes();0===n.length&&e.reportError(`Union type ${t.name} must define one or more member types.`,[t.astNode,...t.extensionASTNodes]);const r=Object.create(null);for(const i of n)r[i.name]?e.reportError(`Union type ${t.name} can only include type ${i.name} once.`,vr(t,i.name)):(r[i.name]=!0,wt(i)||e.reportError(`Union type ${t.name} can only include Object types, it cannot include ${Le(i)}.`,vr(t,String(i))))}function pr(e,t){const n=t.getValues();0===n.length&&e.reportError(`Enum type ${t.name} must define one or more values.`,[t.astNode,...t.extensionASTNodes]);for(const r of n)ar(e,r)}function hr(e,t){const n=Object.values(t.getFields());0===n.length&&e.reportError(`Input Object type ${t.name} must define one or more fields.`,[t.astNode,...t.extensionASTNodes]);for(const o of n){var r,i;if(ar(e,o),!It(o.type))e.reportError(`The type of ${t.name}.${o.name} must be Input Type but got: ${Le(o.type)}.`,null===(r=o.astNode)||void 0===r?void 0:r.type);if(dn(o)&&null!=o.deprecationReason)e.reportError(`Required input field ${t.name}.${o.name} cannot be deprecated.`,[yr(o.astNode),null===(i=o.astNode)||void 0===i?void 0:i.type]);t.isOneOf&&mr(t,o,e)}}function mr(e,t,n){var r;At(t.type)&&n.reportError(`OneOf input field ${e.name}.${t.name} must be nullable.`,null===(r=t.astNode)||void 0===r?void 0:r.type);void 0!==t.defaultValue&&n.reportError(`OneOf input field ${e.name}.${t.name} cannot have a default value.`,t.astNode)}function gr(e,t){const{astNode:n,extensionASTNodes:r}=e;return(null!=n?[n,...r]:r).flatMap((e=>{var t;return null!==(t=e.interfaces)&&void 0!==t?t:[]})).filter((e=>e.name.value===t.name))}function vr(e,t){const{astNode:n,extensionASTNodes:r}=e;return(null!=n?[n,...r]:r).flatMap((e=>{var t;return null!==(t=e.types)&&void 0!==t?t:[]})).filter((e=>e.name.value===t))}function yr(e){var t;return null==e||null===(t=e.directives)||void 0===t?void 0:t.find((e=>e.name.value===An.name))}function br(e,t){switch(t.kind){case J.LIST_TYPE:{const n=br(e,t.type);return n&&new Pt(n)}case J.NON_NULL_TYPE:{const n=br(e,t.type);return n&&new jt(n)}case J.NAMED_TYPE:return e.getType(t.name.value)}}class Er{constructor(e,t,n){this._schema=e,this._typeStack=[],this._parentTypeStack=[],this._inputTypeStack=[],this._fieldDefStack=[],this._defaultValueStack=[],this._directive=null,this._argument=null,this._enumValue=null,this._getFieldDef=null!=n?n:xr,t&&(It(t)&&this._inputTypeStack.push(t),Mt(t)&&this._parentTypeStack.push(t),Ot(t)&&this._typeStack.push(t))}get[Symbol.toStringTag](){return"TypeInfo"}getType(){if(this._typeStack.length>0)return this._typeStack[this._typeStack.length-1]}getParentType(){if(this._parentTypeStack.length>0)return this._parentTypeStack[this._parentTypeStack.length-1]}getInputType(){if(this._inputTypeStack.length>0)return this._inputTypeStack[this._inputTypeStack.length-1]}getParentInputType(){if(this._inputTypeStack.length>1)return this._inputTypeStack[this._inputTypeStack.length-2]}getFieldDef(){if(this._fieldDefStack.length>0)return this._fieldDefStack[this._fieldDefStack.length-1]}getDefaultValue(){if(this._defaultValueStack.length>0)return this._defaultValueStack[this._defaultValueStack.length-1]}getDirective(){return this._directive}getArgument(){return this._argument}getEnumValue(){return this._enumValue}enter(e){const t=this._schema;switch(e.kind){case J.SELECTION_SET:{const e=qt(this.getType());this._parentTypeStack.push(Mt(e)?e:void 0);break}case J.FIELD:{const n=this.getParentType();let r,i;n&&(r=this._getFieldDef(t,n,e),r&&(i=r.type)),this._fieldDefStack.push(r),this._typeStack.push(Ot(i)?i:void 0);break}case J.DIRECTIVE:this._directive=t.getDirective(e.name.value);break;case J.OPERATION_DEFINITION:{const n=t.getRootType(e.operation);this._typeStack.push(wt(n)?n:void 0);break}case J.INLINE_FRAGMENT:case J.FRAGMENT_DEFINITION:{const n=e.typeCondition,r=n?br(t,n):qt(this.getType());this._typeStack.push(Ot(r)?r:void 0);break}case J.VARIABLE_DEFINITION:{const n=br(t,e.type);this._inputTypeStack.push(It(n)?n:void 0);break}case J.ARGUMENT:{var n;let t,r;const i=null!==(n=this.getDirective())&&void 0!==n?n:this.getFieldDef();i&&(t=i.args.find((t=>t.name===e.name.value)),t&&(r=t.type)),this._argument=t,this._defaultValueStack.push(t?t.defaultValue:void 0),this._inputTypeStack.push(It(r)?r:void 0);break}case J.LIST:{const e=Ut(this.getInputType()),t=Dt(e)?e.ofType:e;this._defaultValueStack.push(void 0),this._inputTypeStack.push(It(t)?t:void 0);break}case J.OBJECT_FIELD:{const t=qt(this.getInputType());let n,r;Nt(t)&&(r=t.getFields()[e.name.value],r&&(n=r.type)),this._defaultValueStack.push(r?r.defaultValue:void 0),this._inputTypeStack.push(It(n)?n:void 0);break}case J.ENUM:{const t=qt(this.getInputType());let n;_t(t)&&(n=t.getValue(e.value)),this._enumValue=n;break}}}leave(e){switch(e.kind){case J.SELECTION_SET:this._parentTypeStack.pop();break;case J.FIELD:this._fieldDefStack.pop(),this._typeStack.pop();break;case J.DIRECTIVE:this._directive=null;break;case J.OPERATION_DEFINITION:case J.INLINE_FRAGMENT:case J.FRAGMENT_DEFINITION:this._typeStack.pop();break;case J.VARIABLE_DEFINITION:this._inputTypeStack.pop();break;case J.ARGUMENT:this._argument=null,this._defaultValueStack.pop(),this._inputTypeStack.pop();break;case J.LIST:case J.OBJECT_FIELD:this._defaultValueStack.pop(),this._inputTypeStack.pop();break;case J.ENUM:this._enumValue=null}}}function xr(e,t,n){const r=n.name.value;return r===Kn.name&&e.getQueryType()===t?Kn:r===Yn.name&&e.getQueryType()===t?Yn:r===Qn.name&&Mt(t)?Qn:wt(t)||Ct(t)?t.getFields()[r]:void 0}function wr(e,t){return{enter(...n){const r=n[0];e.enter(r);const i=ct(t,r.kind).enter;if(i){const o=i.apply(t,n);return void 0!==o&&(e.leave(r),G(o)&&e.enter(o)),o}},leave(...n){const r=n[0],i=ct(t,r.kind).leave;let o;return i&&(o=i.apply(t,n)),e.leave(r),o}}}function Tr(e){return e.kind===J.OPERATION_DEFINITION||e.kind===J.FRAGMENT_DEFINITION}function Cr(e){return e.kind===J.VARIABLE||e.kind===J.INT||e.kind===J.FLOAT||e.kind===J.STRING||e.kind===J.BOOLEAN||e.kind===J.NULL||e.kind===J.ENUM||e.kind===J.LIST||e.kind===J.OBJECT}function Sr(e){return e.kind===J.SCHEMA_DEFINITION||kr(e)||e.kind===J.DIRECTIVE_DEFINITION}function kr(e){return e.kind===J.SCALAR_TYPE_DEFINITION||e.kind===J.OBJECT_TYPE_DEFINITION||e.kind===J.INTERFACE_TYPE_DEFINITION||e.kind===J.UNION_TYPE_DEFINITION||e.kind===J.ENUM_TYPE_DEFINITION||e.kind===J.INPUT_OBJECT_TYPE_DEFINITION}function _r(e){return e.kind===J.SCHEMA_EXTENSION||Nr(e)}function Nr(e){return e.kind===J.SCALAR_TYPE_EXTENSION||e.kind===J.OBJECT_TYPE_EXTENSION||e.kind===J.INTERFACE_TYPE_EXTENSION||e.kind===J.UNION_TYPE_EXTENSION||e.kind===J.ENUM_TYPE_EXTENSION||e.kind===J.INPUT_OBJECT_TYPE_EXTENSION}function Dr(e){return{Document(t){for(const n of t.definitions)if(!Tr(n)){const t=n.kind===J.SCHEMA_DEFINITION||n.kind===J.SCHEMA_EXTENSION?"schema":'"'+n.name.value+'"';e.reportError(new B(`The ${t} definition is not executable.`,{nodes:n}))}return!1}}}function Ar(e){return{Field(t){const n=e.getParentType();if(n){if(!e.getFieldDef()){const r=e.getSchema(),i=t.name.value;let o=qe("to use an inline fragment on",function(e,t,n){if(!Rt(t))return[];const r=new Set,i=Object.create(null);for(const s of e.getPossibleTypes(t))if(s.getFields()[n]){r.add(s),i[s.name]=1;for(const e of s.getInterfaces()){var o;e.getFields()[n]&&(r.add(e),i[e.name]=(null!==(o=i[e.name])&&void 0!==o?o:0)+1)}}return[...r].sort(((t,n)=>{const r=i[n.name]-i[t.name];return 0!==r?r:Ct(t)&&e.isSubType(t,n)?-1:Ct(n)&&e.isSubType(n,t)?1:Ye(t.name,n.name)})).map((e=>e.name))}(r,n,i));""===o&&(o=qe(function(e,t){if(wt(e)||Ct(e)){return Ze(t,Object.keys(e.getFields()))}return[]}(n,i))),e.reportError(new B(`Cannot query field "${i}" on type "${n.name}".`+o,{nodes:t}))}}}}}function Ir(e){return{InlineFragment(t){const n=t.typeCondition;if(n){const t=br(e.getSchema(),n);if(t&&!Mt(t)){const t=ut(n);e.reportError(new B(`Fragment cannot condition on non composite type "${t}".`,{nodes:n}))}}},FragmentDefinition(t){const n=br(e.getSchema(),t.typeCondition);if(n&&!Mt(n)){const n=ut(t.typeCondition);e.reportError(new B(`Fragment "${t.name.value}" cannot condition on non composite type "${n}".`,{nodes:t.typeCondition}))}}}}function Or(e){return{...Lr(e),Argument(t){const n=e.getArgument(),r=e.getFieldDef(),i=e.getParentType();if(!n&&r&&i){const n=t.name.value,o=Ze(n,r.args.map((e=>e.name)));e.reportError(new B(`Unknown argument "${n}" on field "${i.name}.${r.name}".`+qe(o),{nodes:t}))}}}}function Lr(e){const t=Object.create(null),n=e.getSchema(),r=n?n.getDirectives():Ln;for(const s of r)t[s.name]=s.args.map((e=>e.name));const i=e.getDocument().definitions;for(const s of i)if(s.kind===J.DIRECTIVE_DEFINITION){var o;const e=null!==(o=s.arguments)&&void 0!==o?o:[];t[s.name.value]=e.map((e=>e.name.value))}return{Directive(n){const r=n.name.value,i=t[r];if(n.arguments&&i)for(const t of n.arguments){const n=t.name.value;if(!i.includes(n)){const o=Ze(n,i);e.reportError(new B(`Unknown argument "${n}" on directive "@${r}".`+qe(o),{nodes:t}))}}return!1}}}function Mr(e){const t=Object.create(null),n=e.getSchema(),r=n?n.getDirectives():Ln;for(const o of r)t[o.name]=o.locations;const i=e.getDocument().definitions;for(const o of i)o.kind===J.DIRECTIVE_DEFINITION&&(t[o.name.value]=o.locations.map((e=>e.value)));return{Directive(n,r,i,o,s){const a=n.name.value,l=t[a];if(!l)return void e.reportError(new B(`Unknown directive "@${a}".`,{nodes:n}));const c=function(e){const t=e[e.length-1];switch("kind"in t||M(!1),t.kind){case J.OPERATION_DEFINITION:return function(e){switch(e){case K.QUERY:return Q.QUERY;case K.MUTATION:return Q.MUTATION;case K.SUBSCRIPTION:return Q.SUBSCRIPTION}}(t.operation);case J.FIELD:return Q.FIELD;case J.FRAGMENT_SPREAD:return Q.FRAGMENT_SPREAD;case J.INLINE_FRAGMENT:return Q.INLINE_FRAGMENT;case J.FRAGMENT_DEFINITION:return Q.FRAGMENT_DEFINITION;case J.VARIABLE_DEFINITION:return Q.VARIABLE_DEFINITION;case J.SCHEMA_DEFINITION:case J.SCHEMA_EXTENSION:return Q.SCHEMA;case J.SCALAR_TYPE_DEFINITION:case J.SCALAR_TYPE_EXTENSION:return Q.SCALAR;case J.OBJECT_TYPE_DEFINITION:case J.OBJECT_TYPE_EXTENSION:return Q.OBJECT;case J.FIELD_DEFINITION:return Q.FIELD_DEFINITION;case J.INTERFACE_TYPE_DEFINITION:case J.INTERFACE_TYPE_EXTENSION:return Q.INTERFACE;case J.UNION_TYPE_DEFINITION:case J.UNION_TYPE_EXTENSION:return Q.UNION;case J.ENUM_TYPE_DEFINITION:case J.ENUM_TYPE_EXTENSION:return Q.ENUM;case J.ENUM_VALUE_DEFINITION:return Q.ENUM_VALUE;case J.INPUT_OBJECT_TYPE_DEFINITION:case J.INPUT_OBJECT_TYPE_EXTENSION:return Q.INPUT_OBJECT;case J.INPUT_VALUE_DEFINITION:{const t=e[e.length-3];return"kind"in t||M(!1),t.kind===J.INPUT_OBJECT_TYPE_DEFINITION?Q.INPUT_FIELD_DEFINITION:Q.ARGUMENT_DEFINITION}default:M(!1,"Unexpected kind: "+Le(t.kind))}}(s);c&&!l.includes(c)&&e.reportError(new B(`Directive "@${a}" may not be used on ${c}.`,{nodes:n}))}}}function Rr(e){return{FragmentSpread(t){const n=t.name.value;e.getFragment(n)||e.reportError(new B(`Unknown fragment "${n}".`,{nodes:t.name}))}}}function Fr(e){const t=e.getSchema(),n=t?t.getTypeMap():Object.create(null),r=Object.create(null);for(const o of e.getDocument().definitions)kr(o)&&(r[o.name.value]=!0);const i=[...Object.keys(n),...Object.keys(r)];return{NamedType(t,o,s,a,l){const c=t.name.value;if(!n[c]&&!r[c]){var u;const n=null!==(u=l[2])&&void 0!==u?u:s,r=null!=n&&("kind"in(d=n)&&(Sr(d)||_r(d)));if(r&&Pr.includes(c))return;const o=Ze(c,r?Pr.concat(i):i);e.reportError(new B(`Unknown type "${c}".`+qe(o),{nodes:t}))}var d}}}const Pr=[...wn,...Xn].map((e=>e.name));function jr(e){let t=0;return{Document(e){t=e.definitions.filter((e=>e.kind===J.OPERATION_DEFINITION)).length},OperationDefinition(n){!n.name&&t>1&&e.reportError(new B("This anonymous operation must be the only defined operation.",{nodes:n}))}}}function Vr(e){var t,n,r;const i=e.getSchema(),o=null!==(t=null!==(n=null!==(r=null==i?void 0:i.astNode)&&void 0!==r?r:null==i?void 0:i.getQueryType())&&void 0!==n?n:null==i?void 0:i.getMutationType())&&void 0!==t?t:null==i?void 0:i.getSubscriptionType();let s=0;return{SchemaDefinition(t){o?e.reportError(new B("Cannot define a new schema within a schema extension.",{nodes:t})):(s>0&&e.reportError(new B("Must provide only one schema definition.",{nodes:t})),++s)}}}function Br(e){function t(n,r=Object.create(null),i=0){if(n.kind===J.FRAGMENT_SPREAD){const o=n.name.value;if(!0===r[o])return!1;const s=e.getFragment(o);if(!s)return!1;try{return r[o]=!0,t(s,r,i)}finally{r[o]=void 0}}if(n.kind===J.FIELD&&("fields"===n.name.value||"interfaces"===n.name.value||"possibleTypes"===n.name.value||"inputFields"===n.name.value)&&++i>=3)return!0;if("selectionSet"in n&&n.selectionSet)for(const e of n.selectionSet.selections)if(t(e,r,i))return!0;return!1}return{Field(n){if(("__schema"===n.name.value||"__type"===n.name.value)&&t(n))return e.reportError(new B("Maximum introspection depth exceeded",{nodes:[n]})),!1}}}function $r(e){const t=Object.create(null),n=[],r=Object.create(null);return{OperationDefinition:()=>!1,FragmentDefinition:e=>(i(e),!1)};function i(o){if(t[o.name.value])return;const s=o.name.value;t[s]=!0;const a=e.getFragmentSpreads(o.selectionSet);if(0!==a.length){r[s]=n.length;for(const t of a){const o=t.name.value,s=r[o];if(n.push(t),void 0===s){const t=e.getFragment(o);t&&i(t)}else{const t=n.slice(s),r=t.slice(0,-1).map((e=>'"'+e.name.value+'"')).join(", ");e.reportError(new B(`Cannot spread fragment "${o}" within itself`+(""!==r?` via ${r}.`:"."),{nodes:t}))}n.pop()}r[s]=void 0}}}function Ur(e){let t=Object.create(null);return{OperationDefinition:{enter(){t=Object.create(null)},leave(n){const r=e.getRecursiveVariableUsages(n);for(const{node:i}of r){const r=i.name.value;!0!==t[r]&&e.reportError(new B(n.name?`Variable "$${r}" is not defined by operation "${n.name.value}".`:`Variable "$${r}" is not defined.`,{nodes:[i,n]}))}}},VariableDefinition(e){t[e.variable.name.value]=!0}}}function Hr(e){const t=[],n=[];return{OperationDefinition:e=>(t.push(e),!1),FragmentDefinition:e=>(n.push(e),!1),Document:{leave(){const r=Object.create(null);for(const n of t)for(const t of e.getRecursivelyReferencedFragments(n))r[t.name.value]=!0;for(const t of n){const n=t.name.value;!0!==r[n]&&e.reportError(new B(`Fragment "${n}" is never used.`,{nodes:t}))}}}}}function qr(e){let t=[];return{OperationDefinition:{enter(){t=[]},leave(n){const r=Object.create(null),i=e.getRecursiveVariableUsages(n);for(const{node:e}of i)r[e.name.value]=!0;for(const o of t){const t=o.variable.name.value;!0!==r[t]&&e.reportError(new B(n.name?`Variable "$${t}" is never used in operation "${n.name.value}".`:`Variable "$${t}" is never used.`,{nodes:o}))}}},VariableDefinition(e){t.push(e)}}}function Wr(e){switch(e.kind){case J.OBJECT:return{...e,fields:(t=e.fields,t.map((e=>({...e,value:Wr(e.value)}))).sort(((e,t)=>Ye(e.name.value,t.name.value))))};case J.LIST:return{...e,values:e.values.map(Wr)};case J.INT:case J.FLOAT:case J.STRING:case J.BOOLEAN:case J.NULL:case J.ENUM:case J.VARIABLE:return e}var t}function zr(e){return Array.isArray(e)?e.map((([e,t])=>`subfields "${e}" conflict because `+zr(t))).join(" and "):e}function Gr(e){const t=new ri,n=new ii,r=new Map;return{SelectionSet(i){const o=function(e,t,n,r,i,o){const s=[],[a,l]=ei(e,t,i,o);if(function(e,t,n,r,i,o){for(const[s,a]of Object.entries(o))if(a.length>1)for(let o=0;o[e.value,t])));return n.every((e=>{const t=e.value,n=i.get(e.name.value);return void 0!==n&&Jr(t)===Jr(n)}))}(c,f))return[[o,"they have differing arguments"],[c],[f]]}const m=null==u?void 0:u.type,g=null==p?void 0:p.type;if(m&&g&&Zr(m,g))return[[o,`they return conflicting types "${Le(m)}" and "${Le(g)}"`],[c],[f]];const v=c.selectionSet,y=f.selectionSet;if(v&&y){const i=function(e,t,n,r,i,o,s,a,l){const c=[],[u,d]=ei(e,t,o,s),[f,p]=ei(e,t,a,l);Qr(e,c,t,n,r,i,u,f);for(const h of p)Kr(e,c,t,n,r,i,u,h);for(const h of d)Kr(e,c,t,n,r,i,f,h);for(const h of d)for(const o of p)Yr(e,c,t,n,r,i,h,o);return c}(e,t,n,r,h,qt(m),v,qt(g),y);return function(e,t,n,r){if(e.length>0)return[[t,e.map((([e])=>e))],[n,...e.map((([,e])=>e)).flat()],[r,...e.map((([,,e])=>e)).flat()]]}(i,o,c,f)}}function Jr(e){return ut(Wr(e))}function Zr(e,t){return Dt(e)?!Dt(t)||Zr(e.ofType,t.ofType):!!Dt(t)||(At(e)?!At(t)||Zr(e.ofType,t.ofType):!!At(t)||!(!Lt(e)&&!Lt(t))&&e!==t)}function ei(e,t,n,r){const i=t.get(r);if(i)return i;const o=Object.create(null),s=Object.create(null);ni(e,n,r,o,s);const a=[o,Object.keys(s)];return t.set(r,a),a}function ti(e,t,n){const r=t.get(n.selectionSet);if(r)return r;const i=br(e.getSchema(),n.typeCondition);return ei(e,t,i,n.selectionSet)}function ni(e,t,n,r,i){for(const o of n.selections)switch(o.kind){case J.FIELD:{const e=o.name.value;let n;(wt(t)||Ct(t))&&(n=t.getFields()[e]);const i=o.alias?o.alias.value:e;r[i]||(r[i]=[]),r[i].push([t,o,n]);break}case J.FRAGMENT_SPREAD:i[o.name.value]=!0;break;case J.INLINE_FRAGMENT:{const n=o.typeCondition,s=n?br(e.getSchema(),n):t;ni(e,s,o.selectionSet,r,i);break}}}class ri{constructor(){this._data=new Map}has(e,t,n){var r;const i=null===(r=this._data.get(e))||void 0===r?void 0:r.get(t);return void 0!==i&&(!!n||n===i)}add(e,t,n){const r=this._data.get(e);void 0===r?this._data.set(e,new Map([[t,n]])):r.set(t,n)}}class ii{constructor(){this._orderedPairSet=new ri}has(e,t,n){return ee.name.value)));for(const o of r.args)if(!i.has(o.name)&&tn(o)){const n=Le(o.type);e.reportError(new B(`Field "${r.name}" argument "${o.name}" of type "${n}" is required, but it was not provided.`,{nodes:t}))}}}}}function ci(e){var t;const n=Object.create(null),r=e.getSchema(),i=null!==(t=null==r?void 0:r.getDirectives())&&void 0!==t?t:Ln;for(const a of i)n[a.name]=ze(a.args.filter(tn),(e=>e.name));const o=e.getDocument().definitions;for(const a of o)if(a.kind===J.DIRECTIVE_DEFINITION){var s;const e=null!==(s=a.arguments)&&void 0!==s?s:[];n[a.name.value]=ze(e.filter(ui),(e=>e.name.value))}return{Directive:{leave(t){const r=t.name.value,i=n[r];if(i){var o;const n=null!==(o=t.arguments)&&void 0!==o?o:[],s=new Set(n.map((e=>e.name.value)));for(const[o,a]of Object.entries(i))if(!s.has(o)){const n=Et(a.type)?Le(a.type):ut(a.type);e.reportError(new B(`Directive "@${r}" argument "${o}" of type "${n}" is required, but it was not provided.`,{nodes:t}))}}}}}}function ui(e){return e.type.kind===J.NON_NULL_TYPE&&null==e.defaultValue}function di(e){return{Field(t){const n=e.getType(),r=t.selectionSet;if(n)if(Lt(qt(n))){if(r){const i=t.name.value,o=Le(n);e.reportError(new B(`Field "${i}" must not have a selection since type "${o}" has no subfields.`,{nodes:r}))}}else if(r){if(0===r.selections.length){const r=t.name.value,i=Le(n);e.reportError(new B(`Field "${r}" of type "${i}" must have at least one field selected.`,{nodes:t}))}}else{const r=t.name.value,i=Le(n);e.reportError(new B(`Field "${r}" of type "${i}" must have a selection of subfields. Did you mean "${r} { ... }"?`,{nodes:t}))}}}}function fi(e){return e.map((e=>"number"==typeof e?"["+e.toString()+"]":"."+e)).join("")}function pi(e,t,n){return{prev:e,key:t,typename:n}}function hi(e){const t=[];let n=e;for(;n;)t.push(n.key),n=n.prev;return t.reverse()}function mi(e,t,n=gi){return vi(e,t,n,void 0)}function gi(e,t,n){let r="Invalid value "+Le(t);throw e.length>0&&(r+=` at "value${fi(e)}"`),n.message=r+": "+n.message,n}function vi(e,t,n,r){if(At(t))return null!=e?vi(e,t.ofType,n,r):void n(hi(r),e,new B(`Expected non-nullable type "${Le(t)}" not to be null.`));if(null==e)return null;if(Dt(t)){const i=t.ofType;return Rn(e)?Array.from(e,((e,t)=>{const o=pi(r,t,void 0);return vi(e,i,n,o)})):[vi(e,i,n,r)]}if(Nt(t)){if(!L(e)||Array.isArray(e))return void n(hi(r),e,new B(`Expected type "${t.name}" to be an object.`));const i={},o=t.getFields();for(const s of Object.values(o)){const o=e[s.name];if(void 0!==o)i[s.name]=vi(o,s.type,n,pi(r,s.name,t.name));else if(void 0!==s.defaultValue)i[s.name]=s.defaultValue;else if(At(s.type)){const t=Le(s.type);n(hi(r),e,new B(`Field "${s.name}" of required type "${t}" was not provided.`))}}for(const s of Object.keys(e))if(!o[s]){const i=Ze(s,Object.keys(t.getFields()));n(hi(r),e,new B(`Field "${s}" is not defined by type "${t.name}".`+qe(i)))}if(t.isOneOf){const o=Object.keys(i);1!==o.length&&n(hi(r),e,new B(`Exactly one key must be specified for OneOf type "${t.name}".`));const s=o[0],a=i[s];null===a&&n(hi(r).concat(s),a,new B(`Field "${s}" must be non-null.`))}return i}if(Lt(t)){let o;try{o=t.parseValue(e)}catch(i){return void n(hi(r),e,i instanceof B?i:new B(`Expected type "${t.name}". `+i.message,{originalError:i}))}return void 0===o&&n(hi(r),e,new B(`Expected type "${t.name}".`)),o}M(!1,"Unexpected input type: "+Le(t))}function yi(e,t,n){if(e){if(e.kind===J.VARIABLE){const r=e.name.value;if(null==n||void 0===n[r])return;const i=n[r];if(null===i&&At(t))return;return i}if(At(t)){if(e.kind===J.NULL)return;return yi(e,t.ofType,n)}if(e.kind===J.NULL)return null;if(Dt(t)){const r=t.ofType;if(e.kind===J.LIST){const t=[];for(const i of e.values)if(bi(i,n)){if(At(r))return;t.push(null)}else{const e=yi(i,r,n);if(void 0===e)return;t.push(e)}return t}const i=yi(e,r,n);if(void 0===i)return;return[i]}if(Nt(t)){if(e.kind!==J.OBJECT)return;const r=Object.create(null),i=ze(e.fields,(e=>e.name.value));for(const e of Object.values(t.getFields())){const t=i[e.name];if(!t||bi(t.value,n)){if(void 0!==e.defaultValue)r[e.name]=e.defaultValue;else if(At(e.type))return;continue}const o=yi(t.value,e.type,n);if(void 0===o)return;r[e.name]=o}if(t.isOneOf){const e=Object.keys(r);if(1!==e.length)return;if(null===r[e[0]])return}return r}if(Lt(t)){let i;try{i=t.parseLiteral(e,n)}catch(r){return}if(void 0===i)return;return i}M(!1,"Unexpected input type: "+Le(t))}}function bi(e,t){return e.kind===J.VARIABLE&&(null==t||void 0===t[e.name.value])}function Ei(e,t,n,r){const i=[],o=null==r?void 0:r.maxErrors;try{const r=function(e,t,n,r){const i={};for(const o of t){const t=o.variable.name.value,s=br(e,o.type);if(!It(s)){const e=ut(o.type);r(new B(`Variable "$${t}" expected value of type "${e}" which cannot be used as an input type.`,{nodes:o.type}));continue}if(!Ti(n,t)){if(o.defaultValue)i[t]=yi(o.defaultValue,s);else if(At(s)){const e=Le(s);r(new B(`Variable "$${t}" of required type "${e}" was not provided.`,{nodes:o}))}continue}const a=n[t];if(null===a&&At(s)){const e=Le(s);r(new B(`Variable "$${t}" of non-null type "${e}" must not be null.`,{nodes:o}))}else i[t]=mi(a,s,((e,n,i)=>{let s=`Variable "$${t}" got invalid value `+Le(n);e.length>0&&(s+=` at "${t}${fi(e)}"`),r(new B(s+"; "+i.message,{nodes:o,originalError:i}))}))}return i}(e,t,n,(e=>{if(null!=o&&i.length>=o)throw new B("Too many errors processing variables, error limit reached. Execution aborted.");i.push(e)}));if(0===i.length)return{coerced:r}}catch(s){i.push(s)}return{errors:i}}function xi(e,t,n){var r;const i={},o=ze(null!==(r=t.arguments)&&void 0!==r?r:[],(e=>e.name.value));for(const s of e.args){const e=s.name,r=s.type,a=o[e];if(!a){if(void 0!==s.defaultValue)i[e]=s.defaultValue;else if(At(r))throw new B(`Argument "${e}" of required type "${Le(r)}" was not provided.`,{nodes:t});continue}const l=a.value;let c=l.kind===J.NULL;if(l.kind===J.VARIABLE){const t=l.name.value;if(null==n||!Ti(n,t)){if(void 0!==s.defaultValue)i[e]=s.defaultValue;else if(At(r))throw new B(`Argument "${e}" of required type "${Le(r)}" was provided the variable "$${t}" which was not provided a runtime value.`,{nodes:l});continue}c=null==n[t]}if(c&&At(r))throw new B(`Argument "${e}" of non-null type "${Le(r)}" must not be null.`,{nodes:l});const u=yi(l,r,n);if(void 0===u)throw new B(`Argument "${e}" has invalid value ${ut(l)}.`,{nodes:l});i[e]=u}return i}function wi(e,t,n){var r;const i=null===(r=t.directives)||void 0===r?void 0:r.find((t=>t.name.value===e.name));if(i)return xi(e,i,n)}function Ti(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function Ci(e,t,n,r,i){const o=new Map;return Si(e,t,n,r,i,o,new Set),o}function Si(e,t,n,r,i,o,s){for(const l of i.selections)switch(l.kind){case J.FIELD:{if(!ki(n,l))continue;const e=(a=l).alias?a.alias.value:a.name.value,t=o.get(e);void 0!==t?t.push(l):o.set(e,[l]);break}case J.INLINE_FRAGMENT:if(!ki(n,l)||!_i(e,l,r))continue;Si(e,t,n,r,l.selectionSet,o,s);break;case J.FRAGMENT_SPREAD:{const i=l.name.value;if(s.has(i)||!ki(n,l))continue;s.add(i);const a=t[i];if(!a||!_i(e,a,r))continue;Si(e,t,n,r,a.selectionSet,o,s);break}}var a}function ki(e,t){const n=wi(Nn,t,e);if(!0===(null==n?void 0:n.if))return!1;const r=wi(_n,t,e);return!1!==(null==r?void 0:r.if)}function _i(e,t,n){const r=t.typeCondition;if(!r)return!0;const i=br(e,r);return i===n||!!Rt(i)&&e.isSubType(i,n)}function Ni(e){return{OperationDefinition(t){if("subscription"===t.operation){const n=e.getSchema(),r=n.getSubscriptionType();if(r){const i=t.name?t.name.value:null,o=Object.create(null),s=e.getDocument(),a=Object.create(null);for(const e of s.definitions)e.kind===J.FRAGMENT_DEFINITION&&(a[e.name.value]=e);const l=Ci(n,a,o,r,t.selectionSet);if(l.size>1){const t=[...l.values()].slice(1).flat();e.reportError(new B(null!=i?`Subscription "${i}" must select only one top level field.`:"Anonymous Subscription must select only one top level field.",{nodes:t}))}for(const t of l.values()){t[0].name.value.startsWith("__")&&e.reportError(new B(null!=i?`Subscription "${i}" must not select an introspection top level field.`:"Anonymous Subscription must not select an introspection top level field.",{nodes:t}))}}}}}}function Di(e,t){const n=new Map;for(const r of e){const e=t(r),i=n.get(e);void 0===i?n.set(e,[r]):i.push(r)}return n}function Ai(e){return{DirectiveDefinition(e){var t;const r=null!==(t=e.arguments)&&void 0!==t?t:[];return n(`@${e.name.value}`,r)},InterfaceTypeDefinition:t,InterfaceTypeExtension:t,ObjectTypeDefinition:t,ObjectTypeExtension:t};function t(e){var t;const r=e.name.value,i=null!==(t=e.fields)&&void 0!==t?t:[];for(const s of i){var o;n(`${r}.${s.name.value}`,null!==(o=s.arguments)&&void 0!==o?o:[])}return!1}function n(t,n){const r=Di(n,(e=>e.name.value));for(const[i,o]of r)o.length>1&&e.reportError(new B(`Argument "${t}(${i}:)" can only be defined once.`,{nodes:o.map((e=>e.name))}));return!1}}function Ii(e){return{Field:t,Directive:t};function t(t){var n;const r=Di(null!==(n=t.arguments)&&void 0!==n?n:[],(e=>e.name.value));for(const[i,o]of r)o.length>1&&e.reportError(new B(`There can be only one argument named "${i}".`,{nodes:o.map((e=>e.name))}))}}function Oi(e){const t=Object.create(null),n=e.getSchema();return{DirectiveDefinition(r){const i=r.name.value;if(null==n||!n.getDirective(i))return t[i]?e.reportError(new B(`There can be only one directive named "@${i}".`,{nodes:[t[i],r.name]})):t[i]=r.name,!1;e.reportError(new B(`Directive "@${i}" already exists in the schema. It cannot be redefined.`,{nodes:r.name}))}}}function Li(e){const t=Object.create(null),n=e.getSchema(),r=n?n.getDirectives():Ln;for(const a of r)t[a.name]=!a.isRepeatable;const i=e.getDocument().definitions;for(const a of i)a.kind===J.DIRECTIVE_DEFINITION&&(t[a.name.value]=!a.repeatable);const o=Object.create(null),s=Object.create(null);return{enter(n){if(!("directives"in n)||!n.directives)return;let r;if(n.kind===J.SCHEMA_DEFINITION||n.kind===J.SCHEMA_EXTENSION)r=o;else if(kr(n)||Nr(n)){const e=n.name.value;r=s[e],void 0===r&&(s[e]=r=Object.create(null))}else r=Object.create(null);for(const i of n.directives){const n=i.name.value;t[n]&&(r[n]?e.reportError(new B(`The directive "@${n}" can only be used once at this location.`,{nodes:[r[n],i]})):r[n]=i)}}}}function Mi(e){const t=e.getSchema(),n=t?t.getTypeMap():Object.create(null),r=Object.create(null);return{EnumTypeDefinition:i,EnumTypeExtension:i};function i(t){var i;const o=t.name.value;r[o]||(r[o]=Object.create(null));const s=null!==(i=t.values)&&void 0!==i?i:[],a=r[o];for(const r of s){const t=r.name.value,i=n[o];_t(i)&&i.getValue(t)?e.reportError(new B(`Enum value "${o}.${t}" already exists in the schema. It cannot also be defined in this type extension.`,{nodes:r.name})):a[t]?e.reportError(new B(`Enum value "${o}.${t}" can only be defined once.`,{nodes:[a[t],r.name]})):a[t]=r.name}return!1}}function Ri(e){const t=e.getSchema(),n=t?t.getTypeMap():Object.create(null),r=Object.create(null);return{InputObjectTypeDefinition:i,InputObjectTypeExtension:i,InterfaceTypeDefinition:i,InterfaceTypeExtension:i,ObjectTypeDefinition:i,ObjectTypeExtension:i};function i(t){var i;const o=t.name.value;r[o]||(r[o]=Object.create(null));const s=null!==(i=t.fields)&&void 0!==i?i:[],a=r[o];for(const r of s){const t=r.name.value;Fi(n[o],t)?e.reportError(new B(`Field "${o}.${t}" already exists in the schema. It cannot also be defined in this type extension.`,{nodes:r.name})):a[t]?e.reportError(new B(`Field "${o}.${t}" can only be defined once.`,{nodes:[a[t],r.name]})):a[t]=r.name}return!1}}function Fi(e,t){return!!(wt(e)||Ct(e)||Nt(e))&&null!=e.getFields()[t]}function Pi(e){const t=Object.create(null);return{OperationDefinition:()=>!1,FragmentDefinition(n){const r=n.name.value;return t[r]?e.reportError(new B(`There can be only one fragment named "${r}".`,{nodes:[t[r],n.name]})):t[r]=n.name,!1}}}function ji(e){const t=[];let n=Object.create(null);return{ObjectValue:{enter(){t.push(n),n=Object.create(null)},leave(){const e=t.pop();e||M(!1),n=e}},ObjectField(t){const r=t.name.value;n[r]?e.reportError(new B(`There can be only one input field named "${r}".`,{nodes:[n[r],t.name]})):n[r]=t.name}}}function Vi(e){const t=Object.create(null);return{OperationDefinition(n){const r=n.name;return r&&(t[r.value]?e.reportError(new B(`There can be only one operation named "${r.value}".`,{nodes:[t[r.value],r]})):t[r.value]=r),!1},FragmentDefinition:()=>!1}}function Bi(e){const t=e.getSchema(),n=Object.create(null),r=t?{query:t.getQueryType(),mutation:t.getMutationType(),subscription:t.getSubscriptionType()}:{};return{SchemaDefinition:i,SchemaExtension:i};function i(t){var i;const o=null!==(i=t.operationTypes)&&void 0!==i?i:[];for(const s of o){const t=s.operation,i=n[t];r[t]?e.reportError(new B(`Type for ${t} already defined in the schema. It cannot be redefined.`,{nodes:s})):i?e.reportError(new B(`There can be only one ${t} type in schema.`,{nodes:[i,s]})):n[t]=s}return!1}}function $i(e){const t=Object.create(null),n=e.getSchema();return{ScalarTypeDefinition:r,ObjectTypeDefinition:r,InterfaceTypeDefinition:r,UnionTypeDefinition:r,EnumTypeDefinition:r,InputObjectTypeDefinition:r};function r(r){const i=r.name.value;if(null==n||!n.getType(i))return t[i]?e.reportError(new B(`There can be only one type named "${i}".`,{nodes:[t[i],r.name]})):t[i]=r.name,!1;e.reportError(new B(`Type "${i}" already exists in the schema. It cannot also be defined in this type definition.`,{nodes:r.name}))}}function Ui(e){return{OperationDefinition(t){var n;const r=Di(null!==(n=t.variableDefinitions)&&void 0!==n?n:[],(e=>e.variable.name.value));for(const[i,o]of r)o.length>1&&e.reportError(new B(`There can be only one variable named "$${i}".`,{nodes:o.map((e=>e.variable.name))}))}}}function Hi(e){let t={};return{OperationDefinition:{enter(){t={}}},VariableDefinition(e){t[e.variable.name.value]=e},ListValue(t){if(!Dt(Ut(e.getParentInputType())))return qi(e,t),!1},ObjectValue(n){const r=qt(e.getInputType());if(!Nt(r))return qi(e,n),!1;const i=ze(n.fields,(e=>e.name.value));for(const t of Object.values(r.getFields())){if(!i[t.name]&&dn(t)){const i=Le(t.type);e.reportError(new B(`Field "${r.name}.${t.name}" of required type "${i}" was not provided.`,{nodes:n}))}}r.isOneOf&&function(e,t,n,r,i){var o;const s=Object.keys(r);if(1!==s.length)return void e.reportError(new B(`OneOf Input Object "${n.name}" must specify exactly one key.`,{nodes:[t]}));const a=null===(o=r[s[0]])||void 0===o?void 0:o.value,l=!a||a.kind===J.NULL,c=(null==a?void 0:a.kind)===J.VARIABLE;if(l)return void e.reportError(new B(`Field "${n.name}.${s[0]}" must be non-null.`,{nodes:[t]}));if(c){const r=a.name.value;i[r].type.kind!==J.NON_NULL_TYPE&&e.reportError(new B(`Variable "${r}" must be non-nullable to be used for OneOf Input Object "${n.name}".`,{nodes:[t]}))}}(e,n,r,i,t)},ObjectField(t){const n=qt(e.getParentInputType());if(!e.getInputType()&&Nt(n)){const r=Ze(t.name.value,Object.keys(n.getFields()));e.reportError(new B(`Field "${t.name.value}" is not defined by type "${n.name}".`+qe(r),{nodes:t}))}},NullValue(t){const n=e.getInputType();At(n)&&e.reportError(new B(`Expected value of type "${Le(n)}", found ${ut(t)}.`,{nodes:t}))},EnumValue:t=>qi(e,t),IntValue:t=>qi(e,t),FloatValue:t=>qi(e,t),StringValue:t=>qi(e,t),BooleanValue:t=>qi(e,t)}}function qi(e,t){const n=e.getInputType();if(!n)return;const r=qt(n);if(Lt(r))try{if(void 0===r.parseLiteral(t,void 0)){const r=Le(n);e.reportError(new B(`Expected value of type "${r}", found ${ut(t)}.`,{nodes:t}))}}catch(i){const r=Le(n);i instanceof B?e.reportError(i):e.reportError(new B(`Expected value of type "${r}", found ${ut(t)}; `+i.message,{nodes:t,originalError:i}))}else{const r=Le(n);e.reportError(new B(`Expected value of type "${r}", found ${ut(t)}.`,{nodes:t}))}}function Wi(e){return{VariableDefinition(t){const n=br(e.getSchema(),t.type);if(void 0!==n&&!It(n)){const n=t.variable.name.value,r=ut(t.type);e.reportError(new B(`Variable "$${n}" cannot be non-input type "${r}".`,{nodes:t.type}))}}}}function zi(e){let t=Object.create(null);return{OperationDefinition:{enter(){t=Object.create(null)},leave(n){const r=e.getRecursiveVariableUsages(n);for(const{node:i,type:o,defaultValue:s,parentType:a}of r){const n=i.name.value,r=t[n];if(r&&o){const t=e.getSchema(),l=br(t,r.type);if(l&&!Gi(t,l,r.defaultValue,o,s)){const t=Le(l),s=Le(o);e.reportError(new B(`Variable "$${n}" of type "${t}" used in position expecting type "${s}".`,{nodes:[r,i]}))}Nt(a)&&a.isOneOf&&Bt(l)&&e.reportError(new B(`Variable "$${n}" is of type "${l}" but must be non-nullable to be used for OneOf Input Object "${a}".`,{nodes:[r,i]}))}}}},VariableDefinition(e){t[e.variable.name.value]=e}}}function Gi(e,t,n,r,i){if(At(r)&&!At(t)){if(!(null!=n&&n.kind!==J.NULL)&&!(void 0!==i))return!1;return pn(e,t,r.ofType)}return pn(e,t,r)}const Ki=Object.freeze([Br]),Yi=Object.freeze([Dr,Vi,jr,Ni,Fr,Ir,Wi,di,Ar,Pi,Rr,Hr,oi,$r,Ui,Ur,qr,Mr,Li,Or,Ii,Hi,li,zi,Gr,ji,...Ki]),Qi=Object.freeze([Vr,Bi,$i,Mi,Ri,Ai,Oi,Fr,Mr,Li,si,Lr,Ii,ji,ci]);class Xi{constructor(e,t){this._ast=e,this._fragments=void 0,this._fragmentSpreads=new Map,this._recursivelyReferencedFragments=new Map,this._onError=t}get[Symbol.toStringTag](){return"ASTValidationContext"}reportError(e){this._onError(e)}getDocument(){return this._ast}getFragment(e){let t;if(this._fragments)t=this._fragments;else{t=Object.create(null);for(const e of this.getDocument().definitions)e.kind===J.FRAGMENT_DEFINITION&&(t[e.name.value]=e);this._fragments=t}return t[e]}getFragmentSpreads(e){let t=this._fragmentSpreads.get(e);if(!t){t=[];const n=[e];let r;for(;r=n.pop();)for(const e of r.selections)e.kind===J.FRAGMENT_SPREAD?t.push(e):e.selectionSet&&n.push(e.selectionSet);this._fragmentSpreads.set(e,t)}return t}getRecursivelyReferencedFragments(e){let t=this._recursivelyReferencedFragments.get(e);if(!t){t=[];const n=Object.create(null),r=[e.selectionSet];let i;for(;i=r.pop();)for(const e of this.getFragmentSpreads(i)){const i=e.name.value;if(!0!==n[i]){n[i]=!0;const e=this.getFragment(i);e&&(t.push(e),r.push(e.selectionSet))}}this._recursivelyReferencedFragments.set(e,t)}return t}}class Ji extends Xi{constructor(e,t,n){super(e,n),this._schema=t}get[Symbol.toStringTag](){return"SDLValidationContext"}getSchema(){return this._schema}}class Zi extends Xi{constructor(e,t,n,r){super(t,r),this._schema=e,this._typeInfo=n,this._variableUsages=new Map,this._recursiveVariableUsages=new Map}get[Symbol.toStringTag](){return"ValidationContext"}getSchema(){return this._schema}getVariableUsages(e){let t=this._variableUsages.get(e);if(!t){const n=[],r=new Er(this._schema);at(e,wr(r,{VariableDefinition:()=>!1,Variable(e){n.push({node:e,type:r.getInputType(),defaultValue:r.getDefaultValue(),parentType:r.getParentInputType()})}})),t=n,this._variableUsages.set(e,t)}return t}getRecursiveVariableUsages(e){let t=this._recursiveVariableUsages.get(e);if(!t){t=this.getVariableUsages(e);for(const n of this.getRecursivelyReferencedFragments(e))t=t.concat(this.getVariableUsages(n));this._recursiveVariableUsages.set(e,t)}return t}getType(){return this._typeInfo.getType()}getParentType(){return this._typeInfo.getParentType()}getInputType(){return this._typeInfo.getInputType()}getParentInputType(){return this._typeInfo.getParentInputType()}getFieldDef(){return this._typeInfo.getFieldDef()}getDirective(){return this._typeInfo.getDirective()}getArgument(){return this._typeInfo.getArgument()}getEnumValue(){return this._typeInfo.getEnumValue()}}function eo(e,t,n=Yi,r,i=new Er(e)){var o;const s=null!==(o=null==r?void 0:r.maxErrors)&&void 0!==o?o:100;t||I(!1,"Must provide document."),ir(e);const a=Object.freeze({}),l=[],c=new Zi(e,t,i,(e=>{if(l.length>=s)throw l.push(new B("Too many validation errors, error limit reached. Validation aborted.")),a;l.push(e)})),u=lt(n.map((e=>e(c))));try{at(t,wr(i,u))}catch(nL){if(nL!==a)throw nL}return l}function to(e,t,n=Qi){const r=[],i=new Ji(e,t,(e=>{r.push(e)}));return at(e,lt(n.map((e=>e(i))))),r}function no(e){return Promise.all(Object.values(e)).then((t=>{const n=Object.create(null);for(const[r,i]of Object.keys(e).entries())n[i]=t[r];return n}))}class ro extends Error{constructor(e){super("Unexpected error value: "+Le(e)),this.name="NonErrorThrown",this.thrownValue=e}}function io(e,t,n){var r;const i=(o=e)instanceof Error?o:new ro(o);var o,s;return s=i,Array.isArray(s.path)?i:new B(i.message,{nodes:null!==(r=i.nodes)&&void 0!==r?r:t,source:i.source,positions:i.positions,path:n,originalError:i})}const oo=function(e){let t;return function(n,r,i){void 0===t&&(t=new WeakMap);let o=t.get(n);void 0===o&&(o=new WeakMap,t.set(n,o));let s=o.get(r);void 0===s&&(s=new WeakMap,o.set(r,s));let a=s.get(i);return void 0===a&&(a=e(n,r,i),s.set(i,a)),a}}(((e,t,n)=>function(e,t,n,r,i){const o=new Map,s=new Set;for(const a of i)a.selectionSet&&Si(e,t,n,r,a.selectionSet,o,s);return o}(e.schema,e.fragments,e.variableValues,t,n)));function so(e){arguments.length<2||I(!1,"graphql@16 dropped long-deprecated support for positional arguments, please pass an object instead.");const{schema:t,document:n,variableValues:r,rootValue:i}=e;co(t,n,r);const o=uo(e);if(!("schema"in o))return{errors:o};try{const{operation:e}=o,t=function(e,t,n){const r=e.schema.getRootType(t.operation);if(null==r)throw new B(`Schema is not configured to execute ${t.operation} operation.`,{nodes:t});const i=Ci(e.schema,e.fragments,e.variableValues,r,t.selectionSet),o=void 0;switch(t.operation){case K.QUERY:return fo(e,r,n,o,i);case K.MUTATION:return function(e,t,n,r,i){return function(e,t,n){let r=n;for(const i of e)r=O(r)?r.then((e=>t(e,i))):t(r,i);return r}(i.entries(),((i,[o,s])=>{const a=pi(r,o,t.name),l=po(e,t,n,s,a);return void 0===l?i:O(l)?l.then((e=>(i[o]=e,i))):(i[o]=l,i)}),Object.create(null))}(e,r,n,o,i);case K.SUBSCRIPTION:return fo(e,r,n,o,i)}}(o,e,i);return O(t)?t.then((e=>lo(e,o.errors)),(e=>(o.errors.push(e),lo(null,o.errors)))):lo(t,o.errors)}catch(s){return o.errors.push(s),lo(null,o.errors)}}function ao(e){const t=so(e);if(O(t))throw new Error("GraphQL execution failed to complete synchronously.");return t}function lo(e,t){return 0===t.length?{data:e}:{errors:t,data:e}}function co(e,t,n){t||I(!1,"Must provide document."),ir(e),null==n||L(n)||I(!1,"Variables must be provided as an Object where each property is a variable value. Perhaps look to see if an unparsed JSON string was provided.")}function uo(e){var t,n,r;const{schema:i,document:o,rootValue:s,contextValue:a,variableValues:l,operationName:c,fieldResolver:u,typeResolver:d,subscribeFieldResolver:f,options:p}=e;let h;const m=Object.create(null);for(const v of o.definitions)switch(v.kind){case J.OPERATION_DEFINITION:if(null==c){if(void 0!==h)return[new B("Must provide operation name if query contains multiple operations.")];h=v}else(null===(t=v.name)||void 0===t?void 0:t.value)===c&&(h=v);break;case J.FRAGMENT_DEFINITION:m[v.name.value]=v}if(!h)return null!=c?[new B(`Unknown operation named "${c}".`)]:[new B("Must provide an operation.")];const g=Ei(i,null!==(n=h.variableDefinitions)&&void 0!==n?n:[],null!=l?l:{},{maxErrors:null!==(r=null==p?void 0:p.maxCoercionErrors)&&void 0!==r?r:50});return g.errors?g.errors:{schema:i,fragments:m,rootValue:s,contextValue:a,operation:h,variableValues:g.coerced,fieldResolver:null!=u?u:xo,typeResolver:null!=d?d:Eo,subscribeFieldResolver:null!=f?f:xo,errors:[]}}function fo(e,t,n,r,i){const o=Object.create(null);let s=!1;try{for(const[a,l]of i.entries()){const i=po(e,t,n,l,pi(r,a,t.name));void 0!==i&&(o[a]=i,O(i)&&(s=!0))}}catch(a){if(s)return no(o).finally((()=>{throw a}));throw a}return s?no(o):o}function po(e,t,n,r,i){var o;const s=wo(e.schema,t,r[0]);if(!s)return;const a=s.type,l=null!==(o=s.resolve)&&void 0!==o?o:e.fieldResolver,c=ho(e,s,r,t,i);try{const t=xi(s,r[0],e.variableValues),o=l(n,t,e.contextValue,c);let u;return u=O(o)?o.then((t=>go(e,a,r,c,i,t))):go(e,a,r,c,i,o),O(u)?u.then(void 0,(t=>mo(io(t,r,hi(i)),a,e))):u}catch(u){return mo(io(u,r,hi(i)),a,e)}}function ho(e,t,n,r,i){return{fieldName:t.name,fieldNodes:n,returnType:t.type,parentType:r,path:i,schema:e.schema,fragments:e.fragments,rootValue:e.rootValue,operation:e.operation,variableValues:e.variableValues}}function mo(e,t,n){if(At(t))throw e;return n.errors.push(e),null}function go(e,t,n,r,i,o){if(o instanceof Error)throw o;if(At(t)){const s=go(e,t.ofType,n,r,i,o);if(null===s)throw new Error(`Cannot return null for non-nullable field ${r.parentType.name}.${r.fieldName}.`);return s}return null==o?null:Dt(t)?function(e,t,n,r,i,o){if(!Rn(o))throw new B(`Expected Iterable, but did not find one for field "${r.parentType.name}.${r.fieldName}".`);const s=t.ofType;let a=!1;const l=Array.from(o,((t,o)=>{const l=pi(i,o,void 0);try{let i;return i=O(t)?t.then((t=>go(e,s,n,r,l,t))):go(e,s,n,r,l,t),O(i)?(a=!0,i.then(void 0,(t=>mo(io(t,n,hi(l)),s,e)))):i}catch(c){return mo(io(c,n,hi(l)),s,e)}}));return a?Promise.all(l):l}(e,t,n,r,i,o):Lt(t)?function(e,t){const n=e.serialize(t);if(null==n)throw new Error(`Expected \`${Le(e)}.serialize(${Le(t)})\` to return non-nullable value, returned: ${Le(n)}`);return n}(t,o):Rt(t)?function(e,t,n,r,i,o){var s;const a=null!==(s=t.resolveType)&&void 0!==s?s:e.typeResolver,l=e.contextValue,c=a(o,l,r,t);if(O(c))return c.then((s=>yo(e,vo(s,e,t,n,r,o),n,r,i,o)));return yo(e,vo(c,e,t,n,r,o),n,r,i,o)}(e,t,n,r,i,o):wt(t)?yo(e,t,n,r,i,o):void M(!1,"Cannot complete value of unexpected output type: "+Le(t))}function vo(e,t,n,r,i,o){if(null==e)throw new B(`Abstract type "${n.name}" must resolve to an Object type at runtime for field "${i.parentType.name}.${i.fieldName}". Either the "${n.name}" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.`,r);if(wt(e))throw new B("Support for returning GraphQLObjectType from resolveType was removed in graphql-js@16.0.0 please return type name instead.");if("string"!=typeof e)throw new B(`Abstract type "${n.name}" must resolve to an Object type at runtime for field "${i.parentType.name}.${i.fieldName}" with value ${Le(o)}, received "${Le(e)}".`);const s=t.schema.getType(e);if(null==s)throw new B(`Abstract type "${n.name}" was resolved to a type "${e}" that does not exist inside the schema.`,{nodes:r});if(!wt(s))throw new B(`Abstract type "${n.name}" was resolved to a non-object type "${e}".`,{nodes:r});if(!t.schema.isSubType(n,s))throw new B(`Runtime Object type "${s.name}" is not a possible type for "${n.name}".`,{nodes:r});return s}function yo(e,t,n,r,i,o){const s=oo(e,t,n);if(t.isTypeOf){const a=t.isTypeOf(o,e.contextValue,r);if(O(a))return a.then((r=>{if(!r)throw bo(t,o,n);return fo(e,t,o,i,s)}));if(!a)throw bo(t,o,n)}return fo(e,t,o,i,s)}function bo(e,t,n){return new B(`Expected value of type "${e.name}" but got: ${Le(t)}.`,{nodes:n})}const Eo=function(e,t,n,r){if(L(e)&&"string"==typeof e.__typename)return e.__typename;const i=n.schema.getPossibleTypes(r),o=[];for(let s=0;s{for(let t=0;t0)return{errors:c};let u;try{u=je(n)}catch(f){return{errors:[f]}}const d=eo(t,u);return d.length>0?{errors:d}:so({schema:t,document:u,rootValue:r,contextValue:i,variableValues:o,operationName:s,fieldResolver:a,typeResolver:l})}function Co(e){return"function"==typeof(null==e?void 0:e[Symbol.asyncIterator])}async function So(...e){const t=function(e){const t=e[0];return t&&"document"in t?t:{schema:t,document:e[1],rootValue:e[2],contextValue:e[3],variableValues:e[4],operationName:e[5],subscribeFieldResolver:e[6]}}(e),{schema:n,document:r,variableValues:i}=t;co(n,r,i);const o=uo(t);if(!("schema"in o))return{errors:o};try{const e=await async function(e){const{schema:t,fragments:n,operation:r,variableValues:i,rootValue:o}=e,s=t.getSubscriptionType();if(null==s)throw new B("Schema is not configured to execute subscription operation.",{nodes:r});const a=Ci(t,n,i,s,r.selectionSet),[l,c]=[...a.entries()][0],u=wo(t,s,c[0]);if(!u){const e=c[0].name.value;throw new B(`The subscription field "${e}" is not defined.`,{nodes:c})}const d=pi(void 0,l,s.name),f=ho(e,u,c,s,d);try{var p;const t=xi(u,c[0],i),n=e.contextValue,r=null!==(p=u.subscribe)&&void 0!==p?p:e.subscribeFieldResolver,s=await r(o,t,n,f);if(s instanceof Error)throw s;return s}catch(h){throw io(h,c,hi(d))}}(o);if(!Co(e))throw new Error(`Subscription field must return Async Iterable. Received: ${Le(e)}.`);return e}catch(s){if(s instanceof B)return{errors:[s]};throw s}}function ko(e){return{Field(t){const n=e.getFieldDef(),r=null==n?void 0:n.deprecationReason;if(n&&null!=r){const i=e.getParentType();null!=i||M(!1),e.reportError(new B(`The field ${i.name}.${n.name} is deprecated. ${r}`,{nodes:t}))}},Argument(t){const n=e.getArgument(),r=null==n?void 0:n.deprecationReason;if(n&&null!=r){const i=e.getDirective();if(null!=i)e.reportError(new B(`Directive "@${i.name}" argument "${n.name}" is deprecated. ${r}`,{nodes:t}));else{const i=e.getParentType(),o=e.getFieldDef();null!=i&&null!=o||M(!1),e.reportError(new B(`Field "${i.name}.${o.name}" argument "${n.name}" is deprecated. ${r}`,{nodes:t}))}}},ObjectField(t){const n=qt(e.getParentInputType());if(Nt(n)){const r=n.getFields()[t.name.value],i=null==r?void 0:r.deprecationReason;null!=i&&e.reportError(new B(`The input field ${n.name}.${r.name} is deprecated. ${i}`,{nodes:t}))}},EnumValue(t){const n=e.getEnumValue(),r=null==n?void 0:n.deprecationReason;if(n&&null!=r){const i=qt(e.getInputType());null!=i||M(!1),e.reportError(new B(`The enum value "${i.name}.${n.name}" is deprecated. ${r}`,{nodes:t}))}}}}function _o(e){const t={descriptions:!0,specifiedByUrl:!1,directiveIsRepeatable:!1,schemaDescription:!1,inputValueDeprecation:!1,oneOf:!1,...e},n=t.descriptions?"description":"",r=t.specifiedByUrl?"specifiedByURL":"",i=t.directiveIsRepeatable?"isRepeatable":"",o=t.schemaDescription?n:"";function s(e){return t.inputValueDeprecation?e:""}const a=t.oneOf?"isOneOf":"";return`\n query IntrospectionQuery {\n __schema {\n ${o}\n queryType { name kind }\n mutationType { name kind }\n subscriptionType { name kind }\n types {\n ...FullType\n }\n directives {\n name\n ${n}\n ${i}\n locations\n args${s("(includeDeprecated: true)")} {\n ...InputValue\n }\n }\n }\n }\n\n fragment FullType on __Type {\n kind\n name\n ${n}\n ${r}\n ${a}\n fields(includeDeprecated: true) {\n name\n ${n}\n args${s("(includeDeprecated: true)")} {\n ...InputValue\n }\n type {\n ...TypeRef\n }\n isDeprecated\n deprecationReason\n }\n inputFields${s("(includeDeprecated: true)")} {\n ...InputValue\n }\n interfaces {\n ...TypeRef\n }\n enumValues(includeDeprecated: true) {\n name\n ${n}\n isDeprecated\n deprecationReason\n }\n possibleTypes {\n ...TypeRef\n }\n }\n\n fragment InputValue on __InputValue {\n name\n ${n}\n type { ...TypeRef }\n defaultValue\n ${s("isDeprecated")}\n ${s("deprecationReason")}\n }\n\n fragment TypeRef on __Type {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n ofType {\n kind\n name\n }\n }\n }\n }\n }\n }\n }\n }\n }\n }\n `}function No(e,t){L(e)&&L(e.__schema)||I(!1,`Invalid or incomplete introspection result. Ensure that you are passing "data" property of introspection response and no "errors" was returned alongside: ${Le(e)}.`);const n=e.__schema,r=Ge(n.types,(e=>e.name),(e=>function(e){if(null!=e&&null!=e.name&&null!=e.kind)switch(e.kind){case Wn.SCALAR:return new Gt({name:(r=e).name,description:r.description,specifiedByURL:r.specifiedByURL});case Wn.OBJECT:return new Kt({name:(n=e).name,description:n.description,interfaces:()=>f(n),fields:()=>p(n)});case Wn.INTERFACE:return new nn({name:(t=e).name,description:t.description,interfaces:()=>f(t),fields:()=>p(t)});case Wn.UNION:return function(e){if(!e.possibleTypes){const t=Le(e);throw new Error(`Introspection result missing possibleTypes: ${t}.`)}return new rn({name:e.name,description:e.description,types:()=>e.possibleTypes.map(u)})}(e);case Wn.ENUM:return function(e){if(!e.enumValues){const t=Le(e);throw new Error(`Introspection result missing enumValues: ${t}.`)}return new sn({name:e.name,description:e.description,values:Ge(e.enumValues,(e=>e.name),(e=>({description:e.description,deprecationReason:e.deprecationReason})))})}(e);case Wn.INPUT_OBJECT:return function(e){if(!e.inputFields){const t=Le(e);throw new Error(`Introspection result missing inputFields: ${t}.`)}return new cn({name:e.name,description:e.description,fields:()=>m(e.inputFields),isOneOf:e.isOneOf})}(e)}var t;var n;var r;const i=Le(e);throw new Error(`Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: ${i}.`)}(e)));for(const v of[...wn,...Xn])r[v.name]&&(r[v.name]=v);const i=n.queryType?u(n.queryType):null,o=n.mutationType?u(n.mutationType):null,s=n.subscriptionType?u(n.subscriptionType):null,a=n.directives?n.directives.map((function(e){if(!e.args){const t=Le(e);throw new Error(`Introspection result missing directive args: ${t}.`)}if(!e.locations){const t=Le(e);throw new Error(`Introspection result missing directive locations: ${t}.`)}return new kn({name:e.name,description:e.description,isRepeatable:e.isRepeatable,locations:e.locations.slice(),args:m(e.args)})})):[];return new tr({description:n.description,query:i,mutation:o,subscription:s,types:Object.values(r),directives:a,assumeValid:null==t?void 0:t.assumeValid});function l(e){if(e.kind===Wn.LIST){const t=e.ofType;if(!t)throw new Error("Decorated type deeper than introspection query.");return new Pt(l(t))}if(e.kind===Wn.NON_NULL){const t=e.ofType;if(!t)throw new Error("Decorated type deeper than introspection query.");const n=l(t);return new jt($t(n))}return c(e)}function c(e){const t=e.name;if(!t)throw new Error(`Unknown type reference: ${Le(e)}.`);const n=r[t];if(!n)throw new Error(`Invalid or incomplete schema, unknown type: ${t}. Ensure that a full introspection query is used in order to build a client schema.`);return n}function u(e){return Tt(c(e))}function d(e){return St(c(e))}function f(e){if(null===e.interfaces&&e.kind===Wn.INTERFACE)return[];if(!e.interfaces){const t=Le(e);throw new Error(`Introspection result missing interfaces: ${t}.`)}return e.interfaces.map(d)}function p(e){if(!e.fields)throw new Error(`Introspection result missing fields: ${Le(e)}.`);return Ge(e.fields,(e=>e.name),h)}function h(e){const t=l(e.type);if(!Ot(t)){const e=Le(t);throw new Error(`Introspection must provide output type for fields, but received: ${e}.`)}if(!e.args){const t=Le(e);throw new Error(`Introspection result missing field args: ${t}.`)}return{description:e.description,deprecationReason:e.deprecationReason,type:t,args:m(e.args)}}function m(e){return Ge(e,(e=>e.name),g)}function g(e){const t=l(e.type);if(!It(t)){const e=Le(t);throw new Error(`Introspection must provide input type for arguments, but received: ${e}.`)}const n=null!=e.defaultValue?yi(Ve(e.defaultValue),t):void 0;return{description:e.description,type:t,defaultValue:n,deprecationReason:e.deprecationReason}}}function Do(e,t,n){var r,i,o,s;const a=[],l=Object.create(null),c=[];let u;const d=[];for(const A of t.definitions)if(A.kind===J.SCHEMA_DEFINITION)u=A;else if(A.kind===J.SCHEMA_EXTENSION)d.push(A);else if(kr(A))a.push(A);else if(Nr(A)){const e=A.name.value,t=l[e];l[e]=t?t.concat([A]):[A]}else A.kind===J.DIRECTIVE_DEFINITION&&c.push(A);if(0===Object.keys(l).length&&0===a.length&&0===c.length&&0===d.length&&null==u)return e;const f=Object.create(null);for(const A of e.types)f[A.name]=v(A);for(const A of a){var p;const e=A.name.value;f[e]=null!==(p=Ao[e])&&void 0!==p?p:D(A)}const h={query:e.query&&g(e.query),mutation:e.mutation&&g(e.mutation),subscription:e.subscription&&g(e.subscription),...u&&E([u]),...E(d)};return{description:null===(r=u)||void 0===r||null===(i=r.description)||void 0===i?void 0:i.value,...h,types:Object.values(f),directives:[...e.directives.map((function(e){const t=e.toConfig();return new kn({...t,args:Ke(t.args,b)})})),...c.map((function(e){var t;return new kn({name:e.name.value,description:null===(t=e.description)||void 0===t?void 0:t.value,locations:e.locations.map((({value:e})=>e)),isRepeatable:e.repeatable,args:C(e.arguments),astNode:e})}))],extensions:Object.create(null),astNode:null!==(o=u)&&void 0!==o?o:e.astNode,extensionASTNodes:e.extensionASTNodes.concat(d),assumeValid:null!==(s=null==n?void 0:n.assumeValid)&&void 0!==s&&s};function m(e){return Dt(e)?new Pt(m(e.ofType)):At(e)?new jt(m(e.ofType)):g(e)}function g(e){return f[e.name]}function v(e){return Jn(e)||Tn(e)?e:xt(e)?function(e){var t;const n=e.toConfig(),r=null!==(t=l[n.name])&&void 0!==t?t:[];let i=n.specifiedByURL;for(const s of r){var o;i=null!==(o=Oo(s))&&void 0!==o?o:i}return new Gt({...n,specifiedByURL:i,extensionASTNodes:n.extensionASTNodes.concat(r)})}(e):wt(e)?function(e){var t;const n=e.toConfig(),r=null!==(t=l[n.name])&&void 0!==t?t:[];return new Kt({...n,interfaces:()=>[...e.getInterfaces().map(g),..._(r)],fields:()=>({...Ke(n.fields,y),...T(r)}),extensionASTNodes:n.extensionASTNodes.concat(r)})}(e):Ct(e)?function(e){var t;const n=e.toConfig(),r=null!==(t=l[n.name])&&void 0!==t?t:[];return new nn({...n,interfaces:()=>[...e.getInterfaces().map(g),..._(r)],fields:()=>({...Ke(n.fields,y),...T(r)}),extensionASTNodes:n.extensionASTNodes.concat(r)})}(e):kt(e)?function(e){var t;const n=e.toConfig(),r=null!==(t=l[n.name])&&void 0!==t?t:[];return new rn({...n,types:()=>[...e.getTypes().map(g),...N(r)],extensionASTNodes:n.extensionASTNodes.concat(r)})}(e):_t(e)?function(e){var t;const n=e.toConfig(),r=null!==(t=l[e.name])&&void 0!==t?t:[];return new sn({...n,values:{...n.values,...k(r)},extensionASTNodes:n.extensionASTNodes.concat(r)})}(e):Nt(e)?function(e){var t;const n=e.toConfig(),r=null!==(t=l[n.name])&&void 0!==t?t:[];return new cn({...n,fields:()=>({...Ke(n.fields,(e=>({...e,type:m(e.type)}))),...S(r)}),extensionASTNodes:n.extensionASTNodes.concat(r)})}(e):void M(!1,"Unexpected type: "+Le(e))}function y(e){return{...e,type:m(e.type),args:e.args&&Ke(e.args,b)}}function b(e){return{...e,type:m(e.type)}}function E(e){const t={};for(const r of e){var n;const e=null!==(n=r.operationTypes)&&void 0!==n?n:[];for(const n of e)t[n.operation]=x(n.type)}return t}function x(e){var t;const n=e.name.value,r=null!==(t=Ao[n])&&void 0!==t?t:f[n];if(void 0===r)throw new Error(`Unknown type: "${n}".`);return r}function w(e){return e.kind===J.LIST_TYPE?new Pt(w(e.type)):e.kind===J.NON_NULL_TYPE?new jt(w(e.type)):x(e)}function T(e){const t=Object.create(null);for(const i of e){var n;const e=null!==(n=i.fields)&&void 0!==n?n:[];for(const n of e){var r;t[n.name.value]={type:w(n.type),description:null===(r=n.description)||void 0===r?void 0:r.value,args:C(n.arguments),deprecationReason:Io(n),astNode:n}}}return t}function C(e){const t=null!=e?e:[],n=Object.create(null);for(const i of t){var r;const e=w(i.type);n[i.name.value]={type:e,description:null===(r=i.description)||void 0===r?void 0:r.value,defaultValue:yi(i.defaultValue,e),deprecationReason:Io(i),astNode:i}}return n}function S(e){const t=Object.create(null);for(const i of e){var n;const e=null!==(n=i.fields)&&void 0!==n?n:[];for(const n of e){var r;const e=w(n.type);t[n.name.value]={type:e,description:null===(r=n.description)||void 0===r?void 0:r.value,defaultValue:yi(n.defaultValue,e),deprecationReason:Io(n),astNode:n}}}return t}function k(e){const t=Object.create(null);for(const i of e){var n;const e=null!==(n=i.values)&&void 0!==n?n:[];for(const n of e){var r;t[n.name.value]={description:null===(r=n.description)||void 0===r?void 0:r.value,deprecationReason:Io(n),astNode:n}}}return t}function _(e){return e.flatMap((e=>{var t,n;return null!==(t=null===(n=e.interfaces)||void 0===n?void 0:n.map(x))&&void 0!==t?t:[]}))}function N(e){return e.flatMap((e=>{var t,n;return null!==(t=null===(n=e.types)||void 0===n?void 0:n.map(x))&&void 0!==t?t:[]}))}function D(e){var t;const n=e.name.value,r=null!==(t=l[n])&&void 0!==t?t:[];switch(e.kind){case J.OBJECT_TYPE_DEFINITION:{var i;const t=[e,...r];return new Kt({name:n,description:null===(i=e.description)||void 0===i?void 0:i.value,interfaces:()=>_(t),fields:()=>T(t),astNode:e,extensionASTNodes:r})}case J.INTERFACE_TYPE_DEFINITION:{var o;const t=[e,...r];return new nn({name:n,description:null===(o=e.description)||void 0===o?void 0:o.value,interfaces:()=>_(t),fields:()=>T(t),astNode:e,extensionASTNodes:r})}case J.ENUM_TYPE_DEFINITION:{var s;const t=[e,...r];return new sn({name:n,description:null===(s=e.description)||void 0===s?void 0:s.value,values:k(t),astNode:e,extensionASTNodes:r})}case J.UNION_TYPE_DEFINITION:{var a;const t=[e,...r];return new rn({name:n,description:null===(a=e.description)||void 0===a?void 0:a.value,types:()=>N(t),astNode:e,extensionASTNodes:r})}case J.SCALAR_TYPE_DEFINITION:var c;return new Gt({name:n,description:null===(c=e.description)||void 0===c?void 0:c.value,specifiedByURL:Oo(e),astNode:e,extensionASTNodes:r});case J.INPUT_OBJECT_TYPE_DEFINITION:{var u;const t=[e,...r];return new cn({name:n,description:null===(u=e.description)||void 0===u?void 0:u.value,fields:()=>S(t),astNode:e,extensionASTNodes:r,isOneOf:(d=e,Boolean(wi(On,d)))})}}var d}}const Ao=ze([...wn,...Xn],(e=>e.name));function Io(e){const t=wi(An,e);return null==t?void 0:t.reason}function Oo(e){const t=wi(In,e);return null==t?void 0:t.url}function Lo(e,t){null!=e&&e.kind===J.DOCUMENT||I(!1,"Must provide valid Document AST."),!0!==(null==t?void 0:t.assumeValid)&&!0!==(null==t?void 0:t.assumeValidSDL)&&function(e){const t=to(e);if(0!==t.length)throw new Error(t.map((e=>e.message)).join("\n\n"))}(e);const n=Do({description:void 0,types:[],directives:[],extensions:Object.create(null),extensionASTNodes:[],assumeValid:!1},e,t);if(null==n.astNode)for(const i of n.types)switch(i.name){case"Query":n.query=i;break;case"Mutation":n.mutation=i;break;case"Subscription":n.subscription=i}const r=[...n.directives,...Ln.filter((e=>n.directives.every((t=>t.name!==e.name))))];return new tr({...n,directives:r})}function Mo(e,t){const n=Object.create(null);for(const r of Object.keys(e).sort(Ye))n[r]=t(e[r]);return n}function Ro(e){return Fo(e,(e=>e.name))}function Fo(e,t){return e.slice().sort(((e,n)=>Ye(t(e),t(n))))}function Po(e){return!Tn(e)&&!Jn(e)}function jo(e,t,n){const r=e.getDirectives().filter(t),i=Object.values(e.getTypeMap()).filter(n);return[Vo(e),...r.map((e=>function(e){return Go(e)+"directive @"+e.name+qo(e.args)+(e.isRepeatable?" repeatable":"")+" on "+e.locations.join(" | ")}(e))),...i.map((e=>Bo(e)))].filter(Boolean).join("\n\n")}function Vo(e){if(null==e.description&&function(e){const t=e.getQueryType();if(t&&"Query"!==t.name)return!1;const n=e.getMutationType();if(n&&"Mutation"!==n.name)return!1;const r=e.getSubscriptionType();if(r&&"Subscription"!==r.name)return!1;return!0}(e))return;const t=[],n=e.getQueryType();n&&t.push(` query: ${n.name}`);const r=e.getMutationType();r&&t.push(` mutation: ${r.name}`);const i=e.getSubscriptionType();return i&&t.push(` subscription: ${i.name}`),Go(e)+`schema {\n${t.join("\n")}\n}`}function Bo(e){return xt(e)?function(e){return Go(e)+`scalar ${e.name}`+function(e){if(null==e.specifiedByURL)return"";return` @specifiedBy(url: ${ut({kind:J.STRING,value:e.specifiedByURL})})`}(e)}(e):wt(e)?function(e){return Go(e)+`type ${e.name}`+$o(e)+Uo(e)}(e):Ct(e)?function(e){return Go(e)+`interface ${e.name}`+$o(e)+Uo(e)}(e):kt(e)?function(e){const t=e.getTypes(),n=t.length?" = "+t.join(" | "):"";return Go(e)+"union "+e.name+n}(e):_t(e)?function(e){const t=e.getValues().map(((e,t)=>Go(e," ",!t)+" "+e.name+zo(e.deprecationReason)));return Go(e)+`enum ${e.name}`+Ho(t)}(e):Nt(e)?function(e){const t=Object.values(e.getFields()).map(((e,t)=>Go(e," ",!t)+" "+Wo(e)));return Go(e)+`input ${e.name}`+(e.isOneOf?" @oneOf":"")+Ho(t)}(e):void M(!1,"Unexpected type: "+Le(e))}function $o(e){const t=e.getInterfaces();return t.length?" implements "+t.map((e=>e.name)).join(" & "):""}function Uo(e){return Ho(Object.values(e.getFields()).map(((e,t)=>Go(e," ",!t)+" "+e.name+qo(e.args," ")+": "+String(e.type)+zo(e.deprecationReason))))}function Ho(e){return 0!==e.length?" {\n"+e.join("\n")+"\n}":""}function qo(e,t=""){return 0===e.length?"":e.every((e=>!e.description))?"("+e.map(Wo).join(", ")+")":"(\n"+e.map(((e,n)=>Go(e," "+t,!n)+" "+t+Wo(e))).join("\n")+"\n"+t+")"}function Wo(e){const t=Fn(e.defaultValue,e.type);let n=e.name+": "+String(e.type);return t&&(n+=` = ${ut(t)}`),n+zo(e.deprecationReason)}function zo(e){if(null==e)return"";if(e!==Dn){return` @deprecated(reason: ${ut({kind:J.STRING,value:e})})`}return" @deprecated"}function Go(e,t="",n=!0){const{description:r}=e;if(null==r)return"";return(t&&!n?"\n"+t:t)+ut({kind:J.STRING,value:r,block:ce(r)}).replace(/\n/g,"\n"+t)+"\n"}function Ko(e,t,n){if(!e.has(n)){e.add(n);const r=t[n];if(void 0!==r)for(const n of r)Ko(e,t,n)}}function Yo(e){const t=[];return at(e,{FragmentSpread(e){t.push(e.name.value)}}),t}function Qo(e){if("string"==typeof e||I(!1,"Expected name to be a string."),e.startsWith("__"))return new B(`Name "${e}" must not begin with "__", which is reserved by GraphQL introspection.`);try{yt(e)}catch(t){return t}}var Xo,Jo,Zo,es;function ts(e,t){return[...rs(e,t),...ns(e,t)]}function ns(e,t){const n=[],r=hs(e.getDirectives(),t.getDirectives());for(const i of r.removed)n.push({type:Xo.DIRECTIVE_REMOVED,description:`${i.name} was removed.`});for(const[i,o]of r.persisted){const e=hs(i.args,o.args);for(const t of e.added)tn(t)&&n.push({type:Xo.REQUIRED_DIRECTIVE_ARG_ADDED,description:`A required arg ${t.name} on directive ${i.name} was added.`});for(const t of e.removed)n.push({type:Xo.DIRECTIVE_ARG_REMOVED,description:`${t.name} was removed from ${i.name}.`});i.isRepeatable&&!o.isRepeatable&&n.push({type:Xo.DIRECTIVE_REPEATABLE_REMOVED,description:`Repeatable flag was removed from ${i.name}.`});for(const t of i.locations)o.locations.includes(t)||n.push({type:Xo.DIRECTIVE_LOCATION_REMOVED,description:`${t} was removed from ${i.name}.`})}return n}function rs(e,t){const n=[],r=hs(Object.values(e.getTypeMap()),Object.values(t.getTypeMap()));for(const i of r.removed)n.push({type:Xo.TYPE_REMOVED,description:Tn(i)?`Standard scalar ${i.name} was removed because it is not referenced anymore.`:`${i.name} was removed.`});for(const[i,o]of r.persisted)_t(i)&&_t(o)?n.push(...ss(i,o)):kt(i)&&kt(o)?n.push(...os(i,o)):Nt(i)&&Nt(o)?n.push(...is(i,o)):wt(i)&&wt(o)||Ct(i)&&Ct(o)?n.push(...ls(i,o),...as(i,o)):i.constructor!==o.constructor&&n.push({type:Xo.TYPE_CHANGED_KIND,description:`${i.name} changed from ${fs(i)} to ${fs(o)}.`});return n}function is(e,t){const n=[],r=hs(Object.values(e.getFields()),Object.values(t.getFields()));for(const i of r.added)dn(i)?n.push({type:Xo.REQUIRED_INPUT_FIELD_ADDED,description:`A required field ${i.name} on input type ${e.name} was added.`}):n.push({type:Zo.OPTIONAL_INPUT_FIELD_ADDED,description:`An optional field ${i.name} on input type ${e.name} was added.`});for(const i of r.removed)n.push({type:Xo.FIELD_REMOVED,description:`${e.name}.${i.name} was removed.`});for(const[i,o]of r.persisted){ds(i.type,o.type)||n.push({type:Xo.FIELD_CHANGED_KIND,description:`${e.name}.${i.name} changed type from ${String(i.type)} to ${String(o.type)}.`})}return n}function os(e,t){const n=[],r=hs(e.getTypes(),t.getTypes());for(const i of r.added)n.push({type:Zo.TYPE_ADDED_TO_UNION,description:`${i.name} was added to union type ${e.name}.`});for(const i of r.removed)n.push({type:Xo.TYPE_REMOVED_FROM_UNION,description:`${i.name} was removed from union type ${e.name}.`});return n}function ss(e,t){const n=[],r=hs(e.getValues(),t.getValues());for(const i of r.added)n.push({type:Zo.VALUE_ADDED_TO_ENUM,description:`${i.name} was added to enum type ${e.name}.`});for(const i of r.removed)n.push({type:Xo.VALUE_REMOVED_FROM_ENUM,description:`${i.name} was removed from enum type ${e.name}.`});return n}function as(e,t){const n=[],r=hs(e.getInterfaces(),t.getInterfaces());for(const i of r.added)n.push({type:Zo.IMPLEMENTED_INTERFACE_ADDED,description:`${i.name} added to interfaces implemented by ${e.name}.`});for(const i of r.removed)n.push({type:Xo.IMPLEMENTED_INTERFACE_REMOVED,description:`${e.name} no longer implements interface ${i.name}.`});return n}function ls(e,t){const n=[],r=hs(Object.values(e.getFields()),Object.values(t.getFields()));for(const i of r.removed)n.push({type:Xo.FIELD_REMOVED,description:`${e.name}.${i.name} was removed.`});for(const[i,o]of r.persisted){n.push(...cs(e,i,o));us(i.type,o.type)||n.push({type:Xo.FIELD_CHANGED_KIND,description:`${e.name}.${i.name} changed type from ${String(i.type)} to ${String(o.type)}.`})}return n}function cs(e,t,n){const r=[],i=hs(t.args,n.args);for(const o of i.removed)r.push({type:Xo.ARG_REMOVED,description:`${e.name}.${t.name} arg ${o.name} was removed.`});for(const[o,s]of i.persisted){if(ds(o.type,s.type)){if(void 0!==o.defaultValue)if(void 0===s.defaultValue)r.push({type:Zo.ARG_DEFAULT_VALUE_CHANGE,description:`${e.name}.${t.name} arg ${o.name} defaultValue was removed.`});else{const n=ps(o.defaultValue,o.type),i=ps(s.defaultValue,s.type);n!==i&&r.push({type:Zo.ARG_DEFAULT_VALUE_CHANGE,description:`${e.name}.${t.name} arg ${o.name} has changed defaultValue from ${n} to ${i}.`})}}else r.push({type:Xo.ARG_CHANGED_KIND,description:`${e.name}.${t.name} arg ${o.name} has changed type from ${String(o.type)} to ${String(s.type)}.`})}for(const o of i.added)tn(o)?r.push({type:Xo.REQUIRED_ARG_ADDED,description:`A required arg ${o.name} on ${e.name}.${t.name} was added.`}):r.push({type:Zo.OPTIONAL_ARG_ADDED,description:`An optional arg ${o.name} on ${e.name}.${t.name} was added.`});return r}function us(e,t){return Dt(e)?Dt(t)&&us(e.ofType,t.ofType)||At(t)&&us(e,t.ofType):At(e)?At(t)&&us(e.ofType,t.ofType):Ht(t)&&e.name===t.name||At(t)&&us(e,t.ofType)}function ds(e,t){return Dt(e)?Dt(t)&&ds(e.ofType,t.ofType):At(e)?At(t)&&ds(e.ofType,t.ofType)||!At(t)&&ds(e.ofType,t):Ht(t)&&e.name===t.name}function fs(e){return xt(e)?"a Scalar type":wt(e)?"an Object type":Ct(e)?"an Interface type":kt(e)?"a Union type":_t(e)?"an Enum type":Nt(e)?"an Input type":void M(!1,"Unexpected type: "+Le(e))}function ps(e,t){const n=Fn(e,t);return null!=n||M(!1),ut(Wr(n))}function hs(e,t){const n=[],r=[],i=[],o=ze(e,(({name:e})=>e)),s=ze(t,(({name:e})=>e));for(const a of e){const e=s[a.name];void 0===e?r.push(a):i.push([a,e])}for(const a of t)void 0===o[a.name]&&n.push(a);return{added:n,persisted:i,removed:r}}(Jo=Xo||(Xo={})).TYPE_REMOVED="TYPE_REMOVED",Jo.TYPE_CHANGED_KIND="TYPE_CHANGED_KIND",Jo.TYPE_REMOVED_FROM_UNION="TYPE_REMOVED_FROM_UNION",Jo.VALUE_REMOVED_FROM_ENUM="VALUE_REMOVED_FROM_ENUM",Jo.REQUIRED_INPUT_FIELD_ADDED="REQUIRED_INPUT_FIELD_ADDED",Jo.IMPLEMENTED_INTERFACE_REMOVED="IMPLEMENTED_INTERFACE_REMOVED",Jo.FIELD_REMOVED="FIELD_REMOVED",Jo.FIELD_CHANGED_KIND="FIELD_CHANGED_KIND",Jo.REQUIRED_ARG_ADDED="REQUIRED_ARG_ADDED",Jo.ARG_REMOVED="ARG_REMOVED",Jo.ARG_CHANGED_KIND="ARG_CHANGED_KIND",Jo.DIRECTIVE_REMOVED="DIRECTIVE_REMOVED",Jo.DIRECTIVE_ARG_REMOVED="DIRECTIVE_ARG_REMOVED",Jo.REQUIRED_DIRECTIVE_ARG_ADDED="REQUIRED_DIRECTIVE_ARG_ADDED",Jo.DIRECTIVE_REPEATABLE_REMOVED="DIRECTIVE_REPEATABLE_REMOVED",Jo.DIRECTIVE_LOCATION_REMOVED="DIRECTIVE_LOCATION_REMOVED",(es=Zo||(Zo={})).VALUE_ADDED_TO_ENUM="VALUE_ADDED_TO_ENUM",es.TYPE_ADDED_TO_UNION="TYPE_ADDED_TO_UNION",es.OPTIONAL_INPUT_FIELD_ADDED="OPTIONAL_INPUT_FIELD_ADDED",es.OPTIONAL_ARG_ADDED="OPTIONAL_ARG_ADDED",es.IMPLEMENTED_INTERFACE_ADDED="IMPLEMENTED_INTERFACE_ADDED",es.ARG_DEFAULT_VALUE_CHANGE="ARG_DEFAULT_VALUE_CHANGE";const ms=Object.freeze(Object.defineProperty({__proto__:null,BREAK:st,get BreakingChangeType(){return Xo},DEFAULT_DEPRECATION_REASON:Dn,get DangerousChangeType(){return Zo},get DirectiveLocation(){return Q},ExecutableDefinitionsRule:Dr,FieldsOnCorrectTypeRule:Ar,FragmentsOnCompositeTypesRule:Ir,GRAPHQL_MAX_INT:mn,GRAPHQL_MIN_INT:gn,GraphQLBoolean:En,GraphQLDeprecatedDirective:An,GraphQLDirective:kn,GraphQLEnumType:sn,GraphQLError:B,GraphQLFloat:yn,GraphQLID:xn,GraphQLIncludeDirective:_n,GraphQLInputObjectType:cn,GraphQLInt:vn,GraphQLInterfaceType:nn,GraphQLList:Pt,GraphQLNonNull:jt,GraphQLObjectType:Kt,GraphQLOneOfDirective:On,GraphQLScalarType:Gt,GraphQLSchema:tr,GraphQLSkipDirective:Nn,GraphQLSpecifiedByDirective:In,GraphQLString:bn,GraphQLUnionType:rn,get Kind(){return J},KnownArgumentNamesRule:Or,KnownDirectivesRule:Mr,KnownFragmentNamesRule:Rr,KnownTypeNamesRule:Fr,Lexer:de,Location:H,LoneAnonymousOperationRule:jr,LoneSchemaDefinitionRule:Vr,MaxIntrospectionDepthRule:Br,NoDeprecatedCustomRule:ko,NoFragmentCyclesRule:$r,NoSchemaIntrospectionCustomRule:function(e){return{Field(t){const n=qt(e.getType());n&&Jn(n)&&e.reportError(new B(`GraphQL introspection has been disabled, but the requested query contained the field "${t.name.value}".`,{nodes:t}))}}},NoUndefinedVariablesRule:Ur,NoUnusedFragmentsRule:Hr,NoUnusedVariablesRule:qr,get OperationTypeNode(){return K},OverlappingFieldsCanBeMergedRule:Gr,PossibleFragmentSpreadsRule:oi,PossibleTypeExtensionsRule:si,ProvidedRequiredArgumentsRule:li,ScalarLeafsRule:di,SchemaMetaFieldDef:Kn,SingleFieldSubscriptionsRule:Ni,Source:Fe,Token:q,get TokenKind(){return ee},TypeInfo:Er,get TypeKind(){return Wn},TypeMetaFieldDef:Yn,TypeNameMetaFieldDef:Qn,UniqueArgumentDefinitionNamesRule:Ai,UniqueArgumentNamesRule:Ii,UniqueDirectiveNamesRule:Oi,UniqueDirectivesPerLocationRule:Li,UniqueEnumValueNamesRule:Mi,UniqueFieldDefinitionNamesRule:Ri,UniqueFragmentNamesRule:Pi,UniqueInputFieldNamesRule:ji,UniqueOperationNamesRule:Vi,UniqueOperationTypesRule:Bi,UniqueTypeNamesRule:$i,UniqueVariableNamesRule:Ui,ValidationContext:Zi,ValuesOfCorrectTypeRule:Hi,VariablesAreInputTypesRule:Wi,VariablesInAllowedPositionRule:zi,__Directive:Vn,__DirectiveLocation:Bn,__EnumValue:qn,__Field:Un,__InputValue:Hn,__Schema:jn,__Type:$n,__TypeKind:Gn,assertAbstractType:Ft,assertCompositeType:function(e){if(!Mt(e))throw new Error(`Expected ${Le(e)} to be a GraphQL composite type.`);return e},assertDirective:function(e){if(!Sn(e))throw new Error(`Expected ${Le(e)} to be a GraphQL directive.`);return e},assertEnumType:function(e){if(!_t(e))throw new Error(`Expected ${Le(e)} to be a GraphQL Enum type.`);return e},assertEnumValueName:bt,assertInputObjectType:function(e){if(!Nt(e))throw new Error(`Expected ${Le(e)} to be a GraphQL Input Object type.`);return e},assertInputType:function(e){if(!It(e))throw new Error(`Expected ${Le(e)} to be a GraphQL input type.`);return e},assertInterfaceType:St,assertLeafType:function(e){if(!Lt(e))throw new Error(`Expected ${Le(e)} to be a GraphQL leaf type.`);return e},assertListType:function(e){if(!Dt(e))throw new Error(`Expected ${Le(e)} to be a GraphQL List type.`);return e},assertName:yt,assertNamedType:function(e){if(!Ht(e))throw new Error(`Expected ${Le(e)} to be a GraphQL named type.`);return e},assertNonNullType:function(e){if(!At(e))throw new Error(`Expected ${Le(e)} to be a GraphQL Non-Null type.`);return e},assertNullableType:$t,assertObjectType:Tt,assertOutputType:function(e){if(!Ot(e))throw new Error(`Expected ${Le(e)} to be a GraphQL output type.`);return e},assertScalarType:function(e){if(!xt(e))throw new Error(`Expected ${Le(e)} to be a GraphQL Scalar type.`);return e},assertSchema:er,assertType:function(e){if(!Et(e))throw new Error(`Expected ${Le(e)} to be a GraphQL type.`);return e},assertUnionType:function(e){if(!kt(e))throw new Error(`Expected ${Le(e)} to be a GraphQL Union type.`);return e},assertValidName:function(e){const t=Qo(e);if(t)throw t;return e},assertValidSchema:ir,assertWrappingType:function(e){if(!Vt(e))throw new Error(`Expected ${Le(e)} to be a GraphQL wrapping type.`);return e},astFromValue:Fn,buildASTSchema:Lo,buildClientSchema:No,buildSchema:function(e,t){return Lo(je(e,{noLocation:null==t?void 0:t.noLocation,allowLegacyFragmentVariables:null==t?void 0:t.allowLegacyFragmentVariables}),{assumeValidSDL:null==t?void 0:t.assumeValidSDL,assumeValid:null==t?void 0:t.assumeValid})},coerceInputValue:mi,concatAST:function(e){const t=[];for(const n of e)t.push(...n.definitions);return{kind:J.DOCUMENT,definitions:t}},createSourceEventStream:So,defaultFieldResolver:xo,defaultTypeResolver:Eo,doTypesOverlap:hn,execute:so,executeSync:ao,extendSchema:function(e,t,n){er(e),null!=t&&t.kind===J.DOCUMENT||I(!1,"Must provide valid Document AST."),!0!==(null==n?void 0:n.assumeValid)&&!0!==(null==n?void 0:n.assumeValidSDL)&&function(e,t){const n=to(e,t);if(0!==n.length)throw new Error(n.map((e=>e.message)).join("\n\n"))}(t,e);const r=e.toConfig(),i=Do(r,t,n);return r===i?e:new tr(i)},findBreakingChanges:function(e,t){return ts(e,t).filter((e=>e.type in Xo))},findDangerousChanges:function(e,t){return ts(e,t).filter((e=>e.type in Zo))},formatError:function(e){return e.toJSON()},getArgumentValues:xi,getDirectiveValues:wi,getEnterLeaveForKind:ct,getIntrospectionQuery:_o,getLocation:F,getNamedType:qt,getNullableType:Ut,getOperationAST:function(e,t){let n=null;for(const i of e.definitions){var r;if(i.kind===J.OPERATION_DEFINITION)if(null==t){if(n)return null;n=i}else if((null===(r=i.name)||void 0===r?void 0:r.value)===t)return i}return n},getOperationRootType:function(e,t){if("query"===t.operation){const n=e.getQueryType();if(!n)throw new B("Schema does not define the required query root type.",{nodes:t});return n}if("mutation"===t.operation){const n=e.getMutationType();if(!n)throw new B("Schema is not configured for mutations.",{nodes:t});return n}if("subscription"===t.operation){const n=e.getSubscriptionType();if(!n)throw new B("Schema is not configured for subscriptions.",{nodes:t});return n}throw new B("Can only have query, mutation and subscription operations.",{nodes:t})},getVariableValues:Ei,getVisitFn:function(e,t,n){const{enter:r,leave:i}=ct(e,t);return n?i:r},graphql:function(e){return new Promise((t=>t(To(e))))},graphqlSync:function(e){const t=To(e);if(O(t))throw new Error("GraphQL execution failed to complete synchronously.");return t},introspectionFromSchema:function(e,t){const n=ao({schema:e,document:je(_o({specifiedByUrl:!0,directiveIsRepeatable:!0,schemaDescription:!0,inputValueDeprecation:!0,oneOf:!0,...t}))});return!n.errors&&n.data||M(!1),n.data},introspectionTypes:Xn,isAbstractType:Rt,isCompositeType:Mt,isConstValueNode:function e(t){return Cr(t)&&(t.kind===J.LIST?t.values.some(e):t.kind===J.OBJECT?t.fields.some((t=>e(t.value))):t.kind!==J.VARIABLE)},isDefinitionNode:function(e){return Tr(e)||Sr(e)||_r(e)},isDirective:Sn,isEnumType:_t,isEqualType:fn,isExecutableDefinitionNode:Tr,isInputObjectType:Nt,isInputType:It,isInterfaceType:Ct,isIntrospectionType:Jn,isLeafType:Lt,isListType:Dt,isNamedType:Ht,isNonNullType:At,isNullableType:Bt,isObjectType:wt,isOutputType:Ot,isRequiredArgument:tn,isRequiredInputField:dn,isScalarType:xt,isSchema:Zn,isSelectionNode:function(e){return e.kind===J.FIELD||e.kind===J.FRAGMENT_SPREAD||e.kind===J.INLINE_FRAGMENT},isSpecifiedDirective:Mn,isSpecifiedScalarType:Tn,isType:Et,isTypeDefinitionNode:kr,isTypeExtensionNode:Nr,isTypeNode:function(e){return e.kind===J.NAMED_TYPE||e.kind===J.LIST_TYPE||e.kind===J.NON_NULL_TYPE},isTypeSubTypeOf:pn,isTypeSystemDefinitionNode:Sr,isTypeSystemExtensionNode:_r,isUnionType:kt,isValidNameError:Qo,isValueNode:Cr,isWrappingType:Vt,lexicographicSortSchema:function(e){const t=e.toConfig(),n=Ge(Ro(t.types),(e=>e.name),(function(e){if(xt(e)||Jn(e))return e;if(wt(e)){const t=e.toConfig();return new Kt({...t,interfaces:()=>l(t.interfaces),fields:()=>a(t.fields)})}if(Ct(e)){const t=e.toConfig();return new nn({...t,interfaces:()=>l(t.interfaces),fields:()=>a(t.fields)})}if(kt(e)){const t=e.toConfig();return new rn({...t,types:()=>l(t.types)})}if(_t(e)){const t=e.toConfig();return new sn({...t,values:Mo(t.values,(e=>e))})}if(Nt(e)){const t=e.toConfig();return new cn({...t,fields:()=>Mo(t.fields,(e=>({...e,type:r(e.type)})))})}M(!1,"Unexpected type: "+Le(e))}));return new tr({...t,types:Object.values(n),directives:Ro(t.directives).map((function(e){const t=e.toConfig();return new kn({...t,locations:Fo(t.locations,(e=>e)),args:s(t.args)})})),query:o(t.query),mutation:o(t.mutation),subscription:o(t.subscription)});function r(e){return Dt(e)?new Pt(r(e.ofType)):At(e)?new jt(r(e.ofType)):i(e)}function i(e){return n[e.name]}function o(e){return e&&i(e)}function s(e){return Mo(e,(e=>({...e,type:r(e.type)})))}function a(e){return Mo(e,(e=>({...e,type:r(e.type),args:e.args&&s(e.args)})))}function l(e){return Ro(e).map(i)}},locatedError:io,parse:je,parseConstValue:function(e,t){const n=new Be(e,t);n.expectToken(ee.SOF);const r=n.parseConstValueLiteral();return n.expectToken(ee.EOF),r},parseType:function(e,t){const n=new Be(e,t);n.expectToken(ee.SOF);const r=n.parseTypeReference();return n.expectToken(ee.EOF),r},parseValue:Ve,print:ut,printError:function(e){return e.toString()},printIntrospectionSchema:function(e){return jo(e,Mn,Jn)},printLocation:P,printSchema:function(e){return jo(e,(e=>!Mn(e)),Po)},printSourceLocation:j,printType:Bo,recommendedRules:Ki,resolveObjMapThunk:zt,resolveReadonlyArrayThunk:Wt,responsePathAsArray:hi,separateOperations:function(e){const t=[],n=Object.create(null);for(const i of e.definitions)switch(i.kind){case J.OPERATION_DEFINITION:t.push(i);break;case J.FRAGMENT_DEFINITION:n[i.name.value]=Yo(i.selectionSet)}const r=Object.create(null);for(const i of t){const t=new Set;for(const e of Yo(i.selectionSet))Ko(t,n,e);r[i.name?i.name.value:""]={kind:J.DOCUMENT,definitions:e.definitions.filter((e=>e===i||e.kind===J.FRAGMENT_DEFINITION&&t.has(e.name.value)))}}return r},specifiedDirectives:Ln,specifiedRules:Yi,specifiedScalarTypes:wn,stripIgnoredCharacters:function(e){const t=Pe(e)?e:new Fe(e),n=t.body,r=new de(t);let i="",o=!1;for(;r.advance().kind!==ee.EOF;){const e=r.token,t=e.kind,s=!fe(e.kind);o&&(s||e.kind===ee.SPREAD)&&(i+=" ");const a=n.slice(e.start,e.end);t===ee.BLOCK_STRING?i+=ue(e.value,{minimize:!0}):i+=a,o=s}return i},subscribe:async function(e){arguments.length<2||I(!1,"graphql@16 dropped long-deprecated support for positional arguments, please pass an object instead.");const t=await So(e);return Co(t)?function(e,t){const n=e[Symbol.asyncIterator]();async function r(e){if(e.done)return e;try{return{value:await t(e.value),done:!1}}catch(r){if("function"==typeof n.return)try{await n.return()}catch(i){}throw r}}return{next:async()=>r(await n.next()),return:async()=>"function"==typeof n.return?r(await n.return()):{value:void 0,done:!0},async throw(e){if("function"==typeof n.throw)return r(await n.throw(e));throw e},[Symbol.asyncIterator](){return this}}}(t,(t=>so({...e,rootValue:t}))):t},syntaxError:U,typeFromAST:br,validate:eo,validateSchema:rr,valueFromAST:yi,valueFromASTUntyped:vt,version:"16.11.0",versionInfo:A,visit:at,visitInParallel:lt,visitWithTypeInfo:wr},Symbol.toStringTag,{value:"Module"}));var gs=new TextDecoder;function vs(){const e={};return e.promise=new Promise(((t,n)=>{e.resolve=t,e.reject=n})),e}const ys=Symbol(),bs=Symbol();const Es=e=>{const{pushValue:t,asyncIterableIterator:n}=function(){let e=!0;const t=[];let n=vs();const r=vs(),i=async function*(){for(;;)if(t.length>0)yield t.shift();else{const e=await Promise.race([n.promise,r.promise]);if(e===ys)break;if(e!==bs)throw e}}(),o=i.return.bind(i);i.return=(...t)=>(e=!1,r.resolve(ys),o(...t));const s=i.throw.bind(i);return i.throw=t=>(e=!1,r.resolve(t),s(t)),{pushValue:function(r){!1!==e&&(t.push(r),n.resolve(bs),n=vs())},asyncIterableIterator:i}}(),r=e({next:e=>{t(e)},complete:()=>{n.return()},error:e=>{n.throw(e)}}),i=n.return;let o;return n.return=()=>(void 0===o&&(r(),o=i()),o),n};const xs=e=>"object"==typeof e&&null!==e&&"code"in e;const ws=e=>t=>Es((n=>e.subscribe(t,C(T({},n),{error(e){e instanceof CloseEvent?n.error(new Error(`Socket closed with event ${e.code} ${e.reason||""}`.trim())):n.error(e)}})))),Ts=e=>t=>{const n=e.request(t);return Es((e=>n.subscribe(e).unsubscribe))},Cs=(e,t)=>function(n,r){return i=this,o=null,s=function*(){const i=yield new S(t(e.url,{method:"POST",body:JSON.stringify(n),headers:T(T({"content-type":"application/json",accept:"application/json, multipart/mixed"},e.headers),null==r?void 0:r.headers)}).then((e=>async function(e,t){if(!e.ok||!e.body||e.bodyUsed)return e;let n=e.headers.get("content-type");if(!n||!~n.indexOf("multipart/"))return e;let r=n.indexOf("boundary="),i="-";if(~r){let e=r+9,t=n.indexOf(";",e);i=n.slice(e,t>-1?t:void 0).trim().replace(/"/g,"")}return async function*(e,t,n){let r,i,o,s=e.getReader(),a=!n||!1,l=t.length,c="",u=[];try{let e;e:for(;!(e=await s.read()).done;){let n=gs.decode(e.value);r=c.length,c+=n;let s=n.indexOf(t);for(~s?r+=s:r=c.indexOf(t),u=[];~r;){let e=c.slice(0,r),n=c.slice(r+l);if(i){let t=e.indexOf("\r\n\r\n")+4,r=e.lastIndexOf("\r\n",t),i=!1,s=e.slice(t,r>-1?void 0:r),l=String(e.slice(0,t)).trim().split("\r\n"),c={},f=l.length;for(;o=l[--f];o=o.split(": "),c[o.shift().toLowerCase()]=o.join(": "));if(o=c["content-type"],o&&~o.indexOf("application/json"))try{s=JSON.parse(s),i=!0}catch(d){}if(o={headers:c,body:s,json:i},a?yield o:u.push(o),"--"===n.slice(0,2))break e}else t="\r\n"+t,i=l+=2;c=n,r=c.indexOf(t)}u.length&&(yield u)}}finally{u.length&&(yield u),await s.cancel()}}(e.body,`--${i}`,t)}(e,{}))));if("object"!=typeof(o=i)||null===o||!("AsyncGenerator"===o[Symbol.toStringTag]||Symbol.asyncIterator&&Symbol.asyncIterator in o))return yield i.json();var o;try{for(var s,a,l,c=((e,t,n)=>(t=e[x("asyncIterator")])?t.call(e):(e=e[x("iterator")](),t={},(n=(n,r)=>(r=e[n])&&(t[n]=t=>new Promise(((n,i,o)=>(t=r.call(e,t),o=t.done,Promise.resolve(t.value).then((e=>n({value:e,done:o})),i))))))("next"),n("return"),t))(i);s=!(a=yield new S(c.next())).done;s=!1){const e=a.value;if(e.some((e=>!e.json))){const t=e.map((e=>`Headers::\n${e.headers}\n\nBody::\n${e.body}`));throw new Error(`Expected multipart chunks to be of json type. got:\n${t}`)}yield e.map((e=>e.body))}}catch(u){l=[u]}finally{try{s&&(a=c.return)&&(yield new S(a.call(c)))}finally{if(l)throw l[0]}}},a=(e,t,n,r)=>{try{var i=s[e](t),o=(t=i.value)instanceof S,l=i.done;Promise.resolve(o?t[0]:t).then((i=>o?a("return"===e?e:"next",t[1]?{done:i.done,value:i.value}:i,n,r):n({value:i,done:l}))).catch((e=>a("throw",e,n,r)))}catch(nL){r(nL)}},l=e=>c[e]=t=>new Promise(((n,r)=>a(e,t,n,r))),c={},s=s.apply(i,o),c[x("asyncIterator")]=()=>c,l("next"),l("throw"),l("return"),c;var i,o,s,a,l,c};async function Ss(e,t){if(e.wsClient)return ws(e.wsClient);if(e.subscriptionUrl)return async function(e,t){let n;try{const{createClient:r}=await Promise.resolve().then((()=>Vj));return n=r({url:e,connectionParams:t}),ws(n)}catch(r){if(xs(r)&&"MODULE_NOT_FOUND"===r.code)throw new Error("You need to install the 'graphql-ws' package to use websockets when passing a 'subscriptionUrl'");console.error(`Error creating websocket client for ${e}`,r)}}(e.subscriptionUrl,T(T({},e.wsConnectionParams),null==t?void 0:t.headers));const n=e.legacyClient||e.legacyWsClient;return n?Ts(n):void 0}function ks(e){return JSON.stringify(e,null,2)}function _s(e){return e instanceof Error?function(e){return C(T({},e),{message:e.message,stack:e.stack})}(e):e}function Ns(e){return Array.isArray(e)?ks({errors:e.map((e=>_s(e)))}):ks({errors:[_s(e)]})}function Ds(e){return ks(e)}function As(e,t,n){const r=[];if(!e||!t)return{insertions:r,result:t};let i;try{i=je(t)}catch(nL){return{insertions:r,result:t}}const o=n||Is,s=new Er(e);return at(i,{leave(e){s.leave(e)},enter(e){if(s.enter(e),"Field"===e.kind&&!e.selectionSet){const n=Os(function(e){if(e)return e}(s.getType()),o);if(n&&e.loc){const i=function(e,t){let n=t,r=t;for(;n;){const t=e.charCodeAt(n-1);if(10===t||13===t||8232===t||8233===t)break;n--,9!==t&&11!==t&&12!==t&&32!==t&&160!==t&&(r=n)}return e.slice(n,r)}(t,e.loc.start);r.push({index:e.loc.end,string:" "+ut(n).replaceAll("\n","\n"+i)})}}}}),{insertions:r,result:Ls(t,r)}}function Is(e){if(!("getFields"in e))return[];const t=e.getFields();if(t.id)return["id"];if(t.edges)return["edges"];if(t.node)return["node"];const n=[];for(const r of Object.keys(t))Lt(t[r].type)&&n.push(r);return n}function Os(e,t){const n=qt(e);if(!e||Lt(e))return;const r=t(n);return Array.isArray(r)&&0!==r.length&&"getFields"in n?{kind:J.SELECTION_SET,selections:r.map((e=>{const r=n.getFields()[e],i=r?r.type:null;return{kind:J.FIELD,name:{kind:J.NAME,value:e},selectionSet:Os(i,t)}}))}:void 0}function Ls(e,t){if(0===t.length)return e;let n="",r=0;for(const{index:i,string:o}of t)n+=e.slice(r,i)+o,r=i;return n+=e.slice(r),n}function Ms(e,t,n){var r;const i=n?qt(n).name:null,o=[],s=[];for(let a of t){if("FragmentSpread"===a.kind){const t=a.name.value;if(!a.directives||0===a.directives.length){if(s.includes(t))continue;s.push(t)}const n=e[a.name.value];if(n){const{typeCondition:e,directives:t,selectionSet:r}=n;a={kind:J.INLINE_FRAGMENT,typeCondition:e,directives:t,selectionSet:r}}}if(a.kind===J.INLINE_FRAGMENT&&(!a.directives||0===(null==(r=a.directives)?void 0:r.length))){const t=a.typeCondition?a.typeCondition.name.value:null;if(!t||t===i){o.push(...Ms(e,a.selectionSet.selections,n));continue}}o.push(a)}return o}function Rs(e,t){const n=t?new Er(t):null,r=Object.create(null);for(const s of e.definitions)s.kind===J.FRAGMENT_DEFINITION&&(r[s.name.value]=s);const i={SelectionSet(e){const t=n?n.getParentType():null;let{selections:i}=e;return i=Ms(r,i,t),C(T({},e),{selections:i})},FragmentDefinition:()=>null},o=at(e,n?wr(n,i):i);return at(o,{SelectionSet(e){let{selections:t}=e;return t=function(e,t){var n;const r=new Map,i=[];for(const o of e)if("Field"===o.kind){const e=t(o),s=r.get(e);if(null!=(n=o.directives)&&n.length){const e=T({},o);i.push(e)}else if(null!=s&&s.selectionSet&&o.selectionSet)s.selectionSet.selections=[...s.selectionSet.selections,...o.selectionSet.selections];else if(!s){const t=T({},o);r.set(e,t),i.push(t)}}else i.push(o);return i}(t,(e=>e.alias?e.alias.value:e.name.value)),C(T({},e),{selections:t})},FragmentDefinition:()=>null})}class Fs{constructor(e){e?this.storage=e:null===e||"undefined"==typeof window?this.storage=null:this.storage={getItem:localStorage.getItem.bind(localStorage),setItem:localStorage.setItem.bind(localStorage),removeItem:localStorage.removeItem.bind(localStorage),get length(){let e=0;for(const t in localStorage)0===t.indexOf(`${Ps}:`)&&(e+=1);return e},clear(){for(const e in localStorage)0===e.indexOf(`${Ps}:`)&&localStorage.removeItem(e)}}}get(e){if(!this.storage)return null;const t=`${Ps}:${e}`,n=this.storage.getItem(t);return"null"===n||"undefined"===n?(this.storage.removeItem(t),null):n||null}set(e,t){let n=!1,r=null;if(this.storage){const i=`${Ps}:${e}`;if(t)try{this.storage.setItem(i,t)}catch(nL){r=nL instanceof Error?nL:new Error(`${nL}`),n=function(e,t){return t instanceof DOMException&&(22===t.code||1014===t.code||"QuotaExceededError"===t.name||"NS_ERROR_DOM_QUOTA_REACHED"===t.name)&&0!==e.length}(this.storage,nL)}else this.storage.removeItem(i)}return{isQuotaError:n,error:r}}clear(){this.storage&&this.storage.clear()}}const Ps="graphiql";class js{constructor(e,t,n=null){this.key=e,this.storage=t,this.maxSize=n,this.items=this.fetchAll()}get length(){return this.items.length}contains(e){return this.items.some((t=>t.query===e.query&&t.variables===e.variables&&t.headers===e.headers&&t.operationName===e.operationName))}edit(e,t){if("number"==typeof t&&this.items[t]){const n=this.items[t];if(n.query===e.query&&n.variables===e.variables&&n.headers===e.headers&&n.operationName===e.operationName)return this.items.splice(t,1,e),void this.save()}const n=this.items.findIndex((t=>t.query===e.query&&t.variables===e.variables&&t.headers===e.headers&&t.operationName===e.operationName));-1!==n&&(this.items.splice(n,1,e),this.save())}delete(e){const t=this.items.findIndex((t=>t.query===e.query&&t.variables===e.variables&&t.headers===e.headers&&t.operationName===e.operationName));-1!==t&&(this.items.splice(t,1),this.save())}fetchRecent(){return this.items.at(-1)}fetchAll(){const e=this.storage.get(this.key);return e?JSON.parse(e)[this.key]:[]}push(e){const t=[...this.items,e];this.maxSize&&t.length>this.maxSize&&t.shift();for(let n=0;n<5;n++){const e=this.storage.set(this.key,JSON.stringify({[this.key]:t}));if(null!=e&&e.error){if(!e.isQuotaError||!this.maxSize)return;t.shift()}else this.items=t}}save(){this.storage.set(this.key,JSON.stringify({[this.key]:this.items}))}}class Vs{constructor(e,t){this.storage=e,this.maxHistoryLength=t,this.updateHistory=({query:e,variables:t,headers:n,operationName:r})=>{if(!this.shouldSaveQuery(e,t,n,this.history.fetchRecent()))return;this.history.push({query:e,variables:t,headers:n,operationName:r});const i=this.history.items,o=this.favorite.items;this.queries=i.concat(o)},this.deleteHistory=({query:e,variables:t,headers:n,operationName:r,favorite:i},o=!1)=>{function s(i){const o=i.items.find((i=>i.query===e&&i.variables===t&&i.headers===n&&i.operationName===r));o&&i.delete(o)}(i||o)&&s(this.favorite),(!i||o)&&s(this.history),this.queries=[...this.history.items,...this.favorite.items]},this.history=new js("queries",this.storage,this.maxHistoryLength),this.favorite=new js("favorites",this.storage,null),this.queries=[...this.history.fetchAll(),...this.favorite.fetchAll()]}shouldSaveQuery(e,t,n,r){if(!e)return!1;try{je(e)}catch(nL){return!1}return!(e.length>1e5)&&(!r||!(JSON.stringify(e)===JSON.stringify(r.query)&&(JSON.stringify(t)===JSON.stringify(r.variables)&&(JSON.stringify(n)===JSON.stringify(r.headers)||n&&!r.headers)||t&&!r.variables)))}toggleFavorite({query:e,variables:t,headers:n,operationName:r,label:i,favorite:o}){const s={query:e,variables:t,headers:n,operationName:r,label:i};o?(s.favorite=!1,this.favorite.delete(s),this.history.push(s)):(s.favorite=!0,this.favorite.push(s),this.history.delete(s)),this.queries=[...this.history.items,...this.favorite.items]}editLabel({query:e,variables:t,headers:n,operationName:r,label:i,favorite:o},s){const a={query:e,variables:t,headers:n,operationName:r,label:i};o?this.favorite.edit(C(T({},a),{favorite:o}),s):this.history.edit(a,s),this.queries=[...this.history.items,...this.favorite.items]}}function Bs(e){const t=Object.keys(e),n=t.length,r=new Array(n);for(let i=0;i!e.isDeprecated));const n=e.map((e=>({proximity:qs(Hs(e.label),t),entry:e})));return Us(Us(n,(e=>e.proximity<=2)),(e=>!e.entry.isDeprecated)).sort(((e,t)=>(e.entry.isDeprecated?1:0)-(t.entry.isDeprecated?1:0)||e.proximity-t.proximity||e.entry.label.length-t.entry.label.length)).map((e=>e.entry))}(t,Hs(e.string))}function Us(e,t){const n=e.filter(t);return 0===n.length?e:n}function Hs(e){return e.toLowerCase().replaceAll(/\W/g,"")}function qs(e,t){let n=function(e,t){let n,r;const i=[],o=e.length,s=t.length;for(n=0;n<=o;n++)i[n]=[n];for(r=1;r<=s;r++)i[0][r]=r;for(n=1;n<=o;n++)for(r=1;r<=s;r++){const o=e[n-1]===t[r-1]?0:1;i[n][r]=Math.min(i[n-1][r]+1,i[n][r-1]+1,i[n-1][r-1]+o),n>1&&r>1&&e[n-1]===t[r-2]&&e[n-2]===t[r-1]&&(i[n][r]=Math.min(i[n][r],i[n-2][r-2]+o))}return i[o][s]}(t,e);return e.length>t.length&&(n-=e.length-t.length-1,n+=0===e.indexOf(t)?0:.5),n}const Ws=(e,t,n)=>{if(!t)return null!=n?n:e;const r=qt(t);return wt(r)||Nt(r)||Dt(r)||Rt(r)?e+" {\n $1\n}":null!=n?n:e},zs=(e,t,n)=>{if(Dt(t)){const n=qt(t.ofType);return e+`[${Ws("",n,"$1")}]`}return Ws(e,t,n)},Gs=e=>{const t=e.args.filter((e=>e.type.toString().endsWith("!")));if(t.length)return e.name+`(${t.map(((e,t)=>`${e.name}: $${t+1}`))}) ${Ws("",e.type,"\n")}`};var Ks,Ys,Qs,Xs,Js,Zs,ea,ta,na,ra,ia,oa,sa,aa,la,ca,ua,da,fa,pa,ha,ma,ga,va,ya,ba,Ea,xa,wa,Ta,Ca,Sa,ka,_a,Na,Da,Aa,Ia,Oa,La,Ma,Ra,Fa,Pa,ja,Va,Ba,$a,Ua,Ha,qa;(Ks||(Ks={})).is=function(e){return"string"==typeof e},(Ys||(Ys={})).is=function(e){return"string"==typeof e},(Xs=Qs||(Qs={})).MIN_VALUE=-2147483648,Xs.MAX_VALUE=2147483647,Xs.is=function(e){return"number"==typeof e&&Xs.MIN_VALUE<=e&&e<=Xs.MAX_VALUE},(Zs=Js||(Js={})).MIN_VALUE=0,Zs.MAX_VALUE=2147483647,Zs.is=function(e){return"number"==typeof e&&Zs.MIN_VALUE<=e&&e<=Zs.MAX_VALUE},(ta=ea||(ea={})).create=function(e,t){return e===Number.MAX_VALUE&&(e=Js.MAX_VALUE),t===Number.MAX_VALUE&&(t=Js.MAX_VALUE),{line:e,character:t}},ta.is=function(e){var t=e;return hc.objectLiteral(t)&&hc.uinteger(t.line)&&hc.uinteger(t.character)},(ra=na||(na={})).create=function(e,t,n,r){if(hc.uinteger(e)&&hc.uinteger(t)&&hc.uinteger(n)&&hc.uinteger(r))return{start:ea.create(e,t),end:ea.create(n,r)};if(ea.is(e)&&ea.is(t))return{start:e,end:t};throw new Error("Range#create called with invalid arguments[".concat(e,", ").concat(t,", ").concat(n,", ").concat(r,"]"))},ra.is=function(e){var t=e;return hc.objectLiteral(t)&&ea.is(t.start)&&ea.is(t.end)},(oa=ia||(ia={})).create=function(e,t){return{uri:e,range:t}},oa.is=function(e){var t=e;return hc.objectLiteral(t)&&na.is(t.range)&&(hc.string(t.uri)||hc.undefined(t.uri))},(aa=sa||(sa={})).create=function(e,t,n,r){return{targetUri:e,targetRange:t,targetSelectionRange:n,originSelectionRange:r}},aa.is=function(e){var t=e;return hc.objectLiteral(t)&&na.is(t.targetRange)&&hc.string(t.targetUri)&&na.is(t.targetSelectionRange)&&(na.is(t.originSelectionRange)||hc.undefined(t.originSelectionRange))},(ca=la||(la={})).create=function(e,t,n,r){return{red:e,green:t,blue:n,alpha:r}},ca.is=function(e){var t=e;return hc.objectLiteral(t)&&hc.numberRange(t.red,0,1)&&hc.numberRange(t.green,0,1)&&hc.numberRange(t.blue,0,1)&&hc.numberRange(t.alpha,0,1)},(da=ua||(ua={})).create=function(e,t){return{range:e,color:t}},da.is=function(e){var t=e;return hc.objectLiteral(t)&&na.is(t.range)&&la.is(t.color)},(pa=fa||(fa={})).create=function(e,t,n){return{label:e,textEdit:t,additionalTextEdits:n}},pa.is=function(e){var t=e;return hc.objectLiteral(t)&&hc.string(t.label)&&(hc.undefined(t.textEdit)||Da.is(t))&&(hc.undefined(t.additionalTextEdits)||hc.typedArray(t.additionalTextEdits,Da.is))},(ma=ha||(ha={})).Comment="comment",ma.Imports="imports",ma.Region="region",(va=ga||(ga={})).create=function(e,t,n,r,i,o){var s={startLine:e,endLine:t};return hc.defined(n)&&(s.startCharacter=n),hc.defined(r)&&(s.endCharacter=r),hc.defined(i)&&(s.kind=i),hc.defined(o)&&(s.collapsedText=o),s},va.is=function(e){var t=e;return hc.objectLiteral(t)&&hc.uinteger(t.startLine)&&hc.uinteger(t.startLine)&&(hc.undefined(t.startCharacter)||hc.uinteger(t.startCharacter))&&(hc.undefined(t.endCharacter)||hc.uinteger(t.endCharacter))&&(hc.undefined(t.kind)||hc.string(t.kind))},(ba=ya||(ya={})).create=function(e,t){return{location:e,message:t}},ba.is=function(e){var t=e;return hc.defined(t)&&ia.is(t.location)&&hc.string(t.message)},(xa=Ea||(Ea={})).Error=1,xa.Warning=2,xa.Information=3,xa.Hint=4,(Ta=wa||(wa={})).Unnecessary=1,Ta.Deprecated=2,(Ca||(Ca={})).is=function(e){var t=e;return hc.objectLiteral(t)&&hc.string(t.href)},(ka=Sa||(Sa={})).create=function(e,t,n,r,i,o){var s={range:e,message:t};return hc.defined(n)&&(s.severity=n),hc.defined(r)&&(s.code=r),hc.defined(i)&&(s.source=i),hc.defined(o)&&(s.relatedInformation=o),s},ka.is=function(e){var t,n=e;return hc.defined(n)&&na.is(n.range)&&hc.string(n.message)&&(hc.number(n.severity)||hc.undefined(n.severity))&&(hc.integer(n.code)||hc.string(n.code)||hc.undefined(n.code))&&(hc.undefined(n.codeDescription)||hc.string(null===(t=n.codeDescription)||void 0===t?void 0:t.href))&&(hc.string(n.source)||hc.undefined(n.source))&&(hc.undefined(n.relatedInformation)||hc.typedArray(n.relatedInformation,ya.is))},(Na=_a||(_a={})).create=function(e,t){for(var n=[],r=2;r0&&(i.arguments=n),i},Na.is=function(e){var t=e;return hc.defined(t)&&hc.string(t.title)&&hc.string(t.command)},(Aa=Da||(Da={})).replace=function(e,t){return{range:e,newText:t}},Aa.insert=function(e,t){return{range:{start:e,end:e},newText:t}},Aa.del=function(e){return{range:e,newText:""}},Aa.is=function(e){var t=e;return hc.objectLiteral(t)&&hc.string(t.newText)&&na.is(t.range)},(Oa=Ia||(Ia={})).create=function(e,t,n){var r={label:e};return void 0!==t&&(r.needsConfirmation=t),void 0!==n&&(r.description=n),r},Oa.is=function(e){var t=e;return hc.objectLiteral(t)&&hc.string(t.label)&&(hc.boolean(t.needsConfirmation)||void 0===t.needsConfirmation)&&(hc.string(t.description)||void 0===t.description)},(La||(La={})).is=function(e){var t=e;return hc.string(t)},(Ra=Ma||(Ma={})).replace=function(e,t,n){return{range:e,newText:t,annotationId:n}},Ra.insert=function(e,t,n){return{range:{start:e,end:e},newText:t,annotationId:n}},Ra.del=function(e,t){return{range:e,newText:"",annotationId:t}},Ra.is=function(e){var t=e;return Da.is(t)&&(Ia.is(t.annotationId)||La.is(t.annotationId))},(Pa=Fa||(Fa={})).create=function(e,t){return{textDocument:e,edits:t}},Pa.is=function(e){var t=e;return hc.defined(t)&&Ya.is(t.textDocument)&&Array.isArray(t.edits)},(Va=ja||(ja={})).create=function(e,t,n){var r={kind:"create",uri:e};return void 0===t||void 0===t.overwrite&&void 0===t.ignoreIfExists||(r.options=t),void 0!==n&&(r.annotationId=n),r},Va.is=function(e){var t=e;return t&&"create"===t.kind&&hc.string(t.uri)&&(void 0===t.options||(void 0===t.options.overwrite||hc.boolean(t.options.overwrite))&&(void 0===t.options.ignoreIfExists||hc.boolean(t.options.ignoreIfExists)))&&(void 0===t.annotationId||La.is(t.annotationId))},($a=Ba||(Ba={})).create=function(e,t,n,r){var i={kind:"rename",oldUri:e,newUri:t};return void 0===n||void 0===n.overwrite&&void 0===n.ignoreIfExists||(i.options=n),void 0!==r&&(i.annotationId=r),i},$a.is=function(e){var t=e;return t&&"rename"===t.kind&&hc.string(t.oldUri)&&hc.string(t.newUri)&&(void 0===t.options||(void 0===t.options.overwrite||hc.boolean(t.options.overwrite))&&(void 0===t.options.ignoreIfExists||hc.boolean(t.options.ignoreIfExists)))&&(void 0===t.annotationId||La.is(t.annotationId))},(Ha=Ua||(Ua={})).create=function(e,t,n){var r={kind:"delete",uri:e};return void 0===t||void 0===t.recursive&&void 0===t.ignoreIfNotExists||(r.options=t),void 0!==n&&(r.annotationId=n),r},Ha.is=function(e){var t=e;return t&&"delete"===t.kind&&hc.string(t.uri)&&(void 0===t.options||(void 0===t.options.recursive||hc.boolean(t.options.recursive))&&(void 0===t.options.ignoreIfNotExists||hc.boolean(t.options.ignoreIfNotExists)))&&(void 0===t.annotationId||La.is(t.annotationId))},(qa||(qa={})).is=function(e){var t=e;return t&&(void 0!==t.changes||void 0!==t.documentChanges)&&(void 0===t.documentChanges||t.documentChanges.every((function(e){return hc.string(e.kind)?ja.is(e)||Ba.is(e)||Ua.is(e):Fa.is(e)})))};var Wa,za,Ga,Ka,Ya,Qa,Xa,Ja,Za,el,tl,nl,rl,il,ol,sl,al,ll,cl,ul,dl,fl,pl,hl,ml,gl,vl,yl,bl,El,xl,wl,Tl,Cl,Sl,kl,_l,Nl,Dl,Al,Il,Ol,Ll,Ml,Rl,Fl,Pl,jl,Vl,Bl,$l,Ul,Hl,ql,Wl,zl,Gl,Kl,Yl,Ql,Xl,Jl,Zl,ec,tc,nc,rc,ic,oc,sc,ac,lc,cc,uc,dc,fc=function(){function e(e,t){this.edits=e,this.changeAnnotations=t}return e.prototype.insert=function(e,t,n){var r,i;if(void 0===n?r=Da.insert(e,t):La.is(n)?(i=n,r=Ma.insert(e,t,n)):(this.assertChangeAnnotations(this.changeAnnotations),i=this.changeAnnotations.manage(n),r=Ma.insert(e,t,i)),this.edits.push(r),void 0!==i)return i},e.prototype.replace=function(e,t,n){var r,i;if(void 0===n?r=Da.replace(e,t):La.is(n)?(i=n,r=Ma.replace(e,t,n)):(this.assertChangeAnnotations(this.changeAnnotations),i=this.changeAnnotations.manage(n),r=Ma.replace(e,t,i)),this.edits.push(r),void 0!==i)return i},e.prototype.delete=function(e,t){var n,r;if(void 0===t?n=Da.del(e):La.is(t)?(r=t,n=Ma.del(e,t)):(this.assertChangeAnnotations(this.changeAnnotations),r=this.changeAnnotations.manage(t),n=Ma.del(e,r)),this.edits.push(n),void 0!==r)return r},e.prototype.add=function(e){this.edits.push(e)},e.prototype.all=function(){return this.edits},e.prototype.clear=function(){this.edits.splice(0,this.edits.length)},e.prototype.assertChangeAnnotations=function(e){if(void 0===e)throw new Error("Text edit change is not configured to manage change annotations.")},e}(),pc=function(){function e(e){this._annotations=void 0===e?Object.create(null):e,this._counter=0,this._size=0}return e.prototype.all=function(){return this._annotations},Object.defineProperty(e.prototype,"size",{get:function(){return this._size},enumerable:!1,configurable:!0}),e.prototype.manage=function(e,t){var n;if(La.is(e)?n=e:(n=this.nextId(),t=e),void 0!==this._annotations[n])throw new Error("Id ".concat(n," is already in use."));if(void 0===t)throw new Error("No annotation provided for id ".concat(n));return this._annotations[n]=t,this._size++,n},e.prototype.nextId=function(){return this._counter++,this._counter.toString()},e}();!function(){function e(e){var t=this;this._textEditChanges=Object.create(null),void 0!==e?(this._workspaceEdit=e,e.documentChanges?(this._changeAnnotations=new pc(e.changeAnnotations),e.changeAnnotations=this._changeAnnotations.all(),e.documentChanges.forEach((function(e){if(Fa.is(e)){var n=new fc(e.edits,t._changeAnnotations);t._textEditChanges[e.textDocument.uri]=n}}))):e.changes&&Object.keys(e.changes).forEach((function(n){var r=new fc(e.changes[n]);t._textEditChanges[n]=r}))):this._workspaceEdit={}}Object.defineProperty(e.prototype,"edit",{get:function(){return this.initDocumentChanges(),void 0!==this._changeAnnotations&&(0===this._changeAnnotations.size?this._workspaceEdit.changeAnnotations=void 0:this._workspaceEdit.changeAnnotations=this._changeAnnotations.all()),this._workspaceEdit},enumerable:!1,configurable:!0}),e.prototype.getTextEditChange=function(e){if(Ya.is(e)){if(this.initDocumentChanges(),void 0===this._workspaceEdit.documentChanges)throw new Error("Workspace edit is not configured for document changes.");var t={uri:e.uri,version:e.version};if(!(r=this._textEditChanges[t.uri])){var n={textDocument:t,edits:i=[]};this._workspaceEdit.documentChanges.push(n),r=new fc(i,this._changeAnnotations),this._textEditChanges[t.uri]=r}return r}if(this.initChanges(),void 0===this._workspaceEdit.changes)throw new Error("Workspace edit is not configured for normal text edit changes.");var r;if(!(r=this._textEditChanges[e])){var i=[];this._workspaceEdit.changes[e]=i,r=new fc(i),this._textEditChanges[e]=r}return r},e.prototype.initDocumentChanges=function(){void 0===this._workspaceEdit.documentChanges&&void 0===this._workspaceEdit.changes&&(this._changeAnnotations=new pc,this._workspaceEdit.documentChanges=[],this._workspaceEdit.changeAnnotations=this._changeAnnotations.all())},e.prototype.initChanges=function(){void 0===this._workspaceEdit.documentChanges&&void 0===this._workspaceEdit.changes&&(this._workspaceEdit.changes=Object.create(null))},e.prototype.createFile=function(e,t,n){if(this.initDocumentChanges(),void 0===this._workspaceEdit.documentChanges)throw new Error("Workspace edit is not configured for document changes.");var r,i,o;if(Ia.is(t)||La.is(t)?r=t:n=t,void 0===r?i=ja.create(e,n):(o=La.is(r)?r:this._changeAnnotations.manage(r),i=ja.create(e,n,o)),this._workspaceEdit.documentChanges.push(i),void 0!==o)return o},e.prototype.renameFile=function(e,t,n,r){if(this.initDocumentChanges(),void 0===this._workspaceEdit.documentChanges)throw new Error("Workspace edit is not configured for document changes.");var i,o,s;if(Ia.is(n)||La.is(n)?i=n:r=n,void 0===i?o=Ba.create(e,t,r):(s=La.is(i)?i:this._changeAnnotations.manage(i),o=Ba.create(e,t,r,s)),this._workspaceEdit.documentChanges.push(o),void 0!==s)return s},e.prototype.deleteFile=function(e,t,n){if(this.initDocumentChanges(),void 0===this._workspaceEdit.documentChanges)throw new Error("Workspace edit is not configured for document changes.");var r,i,o;if(Ia.is(t)||La.is(t)?r=t:n=t,void 0===r?i=Ua.create(e,n):(o=La.is(r)?r:this._changeAnnotations.manage(r),i=Ua.create(e,n,o)),this._workspaceEdit.documentChanges.push(i),void 0!==o)return o}}(),(za=Wa||(Wa={})).create=function(e){return{uri:e}},za.is=function(e){var t=e;return hc.defined(t)&&hc.string(t.uri)},(Ka=Ga||(Ga={})).create=function(e,t){return{uri:e,version:t}},Ka.is=function(e){var t=e;return hc.defined(t)&&hc.string(t.uri)&&hc.integer(t.version)},(Qa=Ya||(Ya={})).create=function(e,t){return{uri:e,version:t}},Qa.is=function(e){var t=e;return hc.defined(t)&&hc.string(t.uri)&&(null===t.version||hc.integer(t.version))},(Ja=Xa||(Xa={})).create=function(e,t,n,r){return{uri:e,languageId:t,version:n,text:r}},Ja.is=function(e){var t=e;return hc.defined(t)&&hc.string(t.uri)&&hc.string(t.languageId)&&hc.integer(t.version)&&hc.string(t.text)},(el=Za||(Za={})).PlainText="plaintext",el.Markdown="markdown",el.is=function(e){var t=e;return t===el.PlainText||t===el.Markdown},(tl||(tl={})).is=function(e){var t=e;return hc.objectLiteral(e)&&Za.is(t.kind)&&hc.string(t.value)},(rl=nl||(nl={})).Text=1,rl.Method=2,rl.Function=3,rl.Constructor=4,rl.Field=5,rl.Variable=6,rl.Class=7,rl.Interface=8,rl.Module=9,rl.Property=10,rl.Unit=11,rl.Value=12,rl.Enum=13,rl.Keyword=14,rl.Snippet=15,rl.Color=16,rl.File=17,rl.Reference=18,rl.Folder=19,rl.EnumMember=20,rl.Constant=21,rl.Struct=22,rl.Event=23,rl.Operator=24,rl.TypeParameter=25,(ol=il||(il={})).PlainText=1,ol.Snippet=2,(sl||(sl={})).Deprecated=1,(ll=al||(al={})).create=function(e,t,n){return{newText:e,insert:t,replace:n}},ll.is=function(e){var t=e;return t&&hc.string(t.newText)&&na.is(t.insert)&&na.is(t.replace)},(ul=cl||(cl={})).asIs=1,ul.adjustIndentation=2,(dl||(dl={})).is=function(e){var t=e;return t&&(hc.string(t.detail)||void 0===t.detail)&&(hc.string(t.description)||void 0===t.description)},(fl||(fl={})).create=function(e){return{label:e}},(pl||(pl={})).create=function(e,t){return{items:e||[],isIncomplete:!!t}},(ml=hl||(hl={})).fromPlainText=function(e){return e.replace(/[\\`*_{}[\]()#+\-.!]/g,"\\$&")},ml.is=function(e){var t=e;return hc.string(t)||hc.objectLiteral(t)&&hc.string(t.language)&&hc.string(t.value)},(gl||(gl={})).is=function(e){var t=e;return!!t&&hc.objectLiteral(t)&&(tl.is(t.contents)||hl.is(t.contents)||hc.typedArray(t.contents,hl.is))&&(void 0===e.range||na.is(e.range))},(vl||(vl={})).create=function(e,t){return t?{label:e,documentation:t}:{label:e}},(yl||(yl={})).create=function(e,t){for(var n=[],r=2;r=0;s--){var a=i[s],l=e.offsetAt(a.range.start),c=e.offsetAt(a.range.end);if(!(c<=o))throw new Error("Overlapping edit");r=r.substring(0,l)+a.newText+r.substring(c,r.length),o=l}return r}}(dc||(dc={}));var hc,mc,gc,vc=function(){function e(e,t,n,r){this._uri=e,this._languageId=t,this._version=n,this._content=r,this._lineOffsets=void 0}return Object.defineProperty(e.prototype,"uri",{get:function(){return this._uri},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"languageId",{get:function(){return this._languageId},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"version",{get:function(){return this._version},enumerable:!1,configurable:!0}),e.prototype.getText=function(e){if(e){var t=this.offsetAt(e.start),n=this.offsetAt(e.end);return this._content.substring(t,n)}return this._content},e.prototype.update=function(e,t){this._content=e.text,this._version=t,this._lineOffsets=void 0},e.prototype.getLineOffsets=function(){if(void 0===this._lineOffsets){for(var e=[],t=this._content,n=!0,r=0;r0&&e.push(t.length),this._lineOffsets=e}return this._lineOffsets},e.prototype.positionAt=function(e){e=Math.max(Math.min(e,this._content.length),0);var t=this.getLineOffsets(),n=0,r=t.length;if(0===r)return ea.create(0,e);for(;ne?r=i:n=i+1}var o=n-1;return ea.create(o,e-t[o])},e.prototype.offsetAt=function(e){var t=this.getLineOffsets();if(e.line>=t.length)return this._content.length;if(e.line<0)return 0;var n=t[e.line],r=e.line+1this._start,this.getCurrentPosition=()=>this._pos,this.eol=()=>this._sourceText.length===this._pos,this.sol=()=>0===this._pos,this.peek=()=>this._sourceText.charAt(this._pos)||null,this.next=()=>{const e=this._sourceText.charAt(this._pos);return this._pos++,e},this.eat=e=>{if(this._testNextCharacter(e))return this._start=this._pos,this._pos++,this._sourceText.charAt(this._pos-1)},this.eatWhile=e=>{let t=this._testNextCharacter(e),n=!1;for(t&&(n=t,this._start=this._pos);t;)this._pos++,t=this._testNextCharacter(e),n=!0;return n},this.eatSpace=()=>this.eatWhile(/[\s\u00a0]/),this.skipToEnd=()=>{this._pos=this._sourceText.length},this.skipTo=e=>{this._pos=e},this.match=(e,t=!0,n=!1)=>{let r=null,i=null;if("string"==typeof e){i=new RegExp(e,n?"i":"g").test(this._sourceText.slice(this._pos,this._pos+e.length)),r=e}else e instanceof RegExp&&(i=this._sourceText.slice(this._pos).match(e),r=null==i?void 0:i[0]);return!(null==i||!("string"==typeof e||i instanceof Array&&this._sourceText.startsWith(i[0],this._pos)))&&(t&&(this._start=this._pos,r&&r.length&&(this._pos+=r.length)),i)},this.backUp=e=>{this._pos-=e},this.column=()=>this._pos,this.indentation=()=>{const e=this._sourceText.match(/\s*/);let t=0;if(e&&0!==e.length){const n=e[0];let r=0;for(;n.length>r;)9===n.charCodeAt(r)?t+=2:t++,r++}return t},this.current=()=>this._sourceText.slice(this._start,this._pos),this._sourceText=e}_testNextCharacter(e){const t=this._sourceText.charAt(this._pos);let n=!1;return n="string"==typeof e?t===e:e instanceof RegExp?e.test(t):e(t),n}}function bc(e){return{ofRule:e}}function Ec(e,t){return{ofRule:e,isList:!0,separator:t}}function xc(e,t){return{style:t,match:t=>t.kind===e}}function wc(e,t){return{style:t||"punctuation",match:t=>"Punctuation"===t.kind&&t.value===e}}const Tc=e=>" "===e||"\t"===e||","===e||"\n"===e||"\r"===e||"\ufeff"===e||" "===e,Cc={Name:/^[_A-Za-z][_0-9A-Za-z]*/,Punctuation:/^(?:!|\$|\(|\)|\.\.\.|:|=|&|@|\[|]|\{|\||\})/,Number:/^-?(?:0|(?:[1-9][0-9]*))(?:\.[0-9]*)?(?:[eE][+-]?[0-9]+)?/,String:/^(?:"""(?:\\"""|[^"]|"[^"]|""[^"])*(?:""")?|"(?:[^"\\]|\\(?:"|\/|\\|b|f|n|r|t|u[0-9a-fA-F]{4}))*"?)/,Comment:/^#.*/},Sc={Document:[Ec("Definition")],Definition(e){switch(e.value){case"{":return"ShortQuery";case"query":return"Query";case"mutation":return"Mutation";case"subscription":return"Subscription";case"fragment":return J.FRAGMENT_DEFINITION;case"schema":return"SchemaDef";case"scalar":return"ScalarDef";case"type":return"ObjectTypeDef";case"interface":return"InterfaceDef";case"union":return"UnionDef";case"enum":return"EnumDef";case"input":return"InputDef";case"extend":return"ExtendDef";case"directive":return"DirectiveDef"}},ShortQuery:["SelectionSet"],Query:[_c("query"),bc(Nc("def")),bc("VariableDefinitions"),Ec("Directive"),"SelectionSet"],Mutation:[_c("mutation"),bc(Nc("def")),bc("VariableDefinitions"),Ec("Directive"),"SelectionSet"],Subscription:[_c("subscription"),bc(Nc("def")),bc("VariableDefinitions"),Ec("Directive"),"SelectionSet"],VariableDefinitions:[wc("("),Ec("VariableDefinition"),wc(")")],VariableDefinition:["Variable",wc(":"),"Type",bc("DefaultValue")],Variable:[wc("$","variable"),Nc("variable")],DefaultValue:[wc("="),"Value"],SelectionSet:[wc("{"),Ec("Selection"),wc("}")],Selection:(e,t)=>"..."===e.value?t.match(/[\s\u00a0,]*(on\b|@|{)/,!1)?"InlineFragment":"FragmentSpread":t.match(/[\s\u00a0,]*:/,!1)?"AliasedField":"Field",AliasedField:[Nc("property"),wc(":"),Nc("qualifier"),bc("Arguments"),Ec("Directive"),bc("SelectionSet")],Field:[Nc("property"),bc("Arguments"),Ec("Directive"),bc("SelectionSet")],Arguments:[wc("("),Ec("Argument"),wc(")")],Argument:[Nc("attribute"),wc(":"),"Value"],FragmentSpread:[wc("..."),Nc("def"),Ec("Directive")],InlineFragment:[wc("..."),bc("TypeCondition"),Ec("Directive"),"SelectionSet"],FragmentDefinition:[_c("fragment"),bc(function(e,t){const n=e.match;return e.match=e=>{let r=!1;return n&&(r=n(e)),r&&t.every((t=>t.match&&!t.match(e)))},e}(Nc("def"),[_c("on")])),"TypeCondition",Ec("Directive"),"SelectionSet"],TypeCondition:[_c("on"),"NamedType"],Value(e){switch(e.kind){case"Number":return"NumberValue";case"String":return"StringValue";case"Punctuation":switch(e.value){case"[":return"ListValue";case"{":return"ObjectValue";case"$":return"Variable";case"&":return"NamedType"}return null;case"Name":switch(e.value){case"true":case"false":return"BooleanValue"}return"null"===e.value?"NullValue":"EnumValue"}},NumberValue:[xc("Number","number")],StringValue:[{style:"string",match:e=>"String"===e.kind,update(e,t){t.value.startsWith('"""')&&(e.inBlockstring=!t.value.slice(3).endsWith('"""'))}}],BooleanValue:[xc("Name","builtin")],NullValue:[xc("Name","keyword")],EnumValue:[Nc("string-2")],ListValue:[wc("["),Ec("Value"),wc("]")],ObjectValue:[wc("{"),Ec("ObjectField"),wc("}")],ObjectField:[Nc("attribute"),wc(":"),"Value"],Type:e=>"["===e.value?"ListType":"NonNullType",ListType:[wc("["),"Type",wc("]"),bc(wc("!"))],NonNullType:["NamedType",bc(wc("!"))],NamedType:[(kc="atom",{style:kc,match:e=>"Name"===e.kind,update(e,t){var n;(null===(n=e.prevState)||void 0===n?void 0:n.prevState)&&(e.name=t.value,e.prevState.prevState.type=t.value)}})],Directive:[wc("@","meta"),Nc("meta"),bc("Arguments")],DirectiveDef:[_c("directive"),wc("@","meta"),Nc("meta"),bc("ArgumentsDef"),_c("on"),Ec("DirectiveLocation",wc("|"))],InterfaceDef:[_c("interface"),Nc("atom"),bc("Implements"),Ec("Directive"),wc("{"),Ec("FieldDef"),wc("}")],Implements:[_c("implements"),Ec("NamedType",wc("&"))],DirectiveLocation:[Nc("string-2")],SchemaDef:[_c("schema"),Ec("Directive"),wc("{"),Ec("OperationTypeDef"),wc("}")],OperationTypeDef:[Nc("keyword"),wc(":"),Nc("atom")],ScalarDef:[_c("scalar"),Nc("atom"),Ec("Directive")],ObjectTypeDef:[_c("type"),Nc("atom"),bc("Implements"),Ec("Directive"),wc("{"),Ec("FieldDef"),wc("}")],FieldDef:[Nc("property"),bc("ArgumentsDef"),wc(":"),"Type",Ec("Directive")],ArgumentsDef:[wc("("),Ec("InputValueDef"),wc(")")],InputValueDef:[Nc("attribute"),wc(":"),"Type",bc("DefaultValue"),Ec("Directive")],UnionDef:[_c("union"),Nc("atom"),Ec("Directive"),wc("="),Ec("UnionMember",wc("|"))],UnionMember:["NamedType"],EnumDef:[_c("enum"),Nc("atom"),Ec("Directive"),wc("{"),Ec("EnumValueDef"),wc("}")],EnumValueDef:[Nc("string-2"),Ec("Directive")],InputDef:[_c("input"),Nc("atom"),Ec("Directive"),wc("{"),Ec("InputValueDef"),wc("}")],ExtendDef:[_c("extend"),"ExtensionDefinition"],ExtensionDefinition(e){switch(e.value){case"schema":return J.SCHEMA_EXTENSION;case"scalar":return J.SCALAR_TYPE_EXTENSION;case"type":return J.OBJECT_TYPE_EXTENSION;case"interface":return J.INTERFACE_TYPE_EXTENSION;case"union":return J.UNION_TYPE_EXTENSION;case"enum":return J.ENUM_TYPE_EXTENSION;case"input":return J.INPUT_OBJECT_TYPE_EXTENSION}},[J.SCHEMA_EXTENSION]:["SchemaDef"],[J.SCALAR_TYPE_EXTENSION]:["ScalarDef"],[J.OBJECT_TYPE_EXTENSION]:["ObjectTypeDef"],[J.INTERFACE_TYPE_EXTENSION]:["InterfaceDef"],[J.UNION_TYPE_EXTENSION]:["UnionDef"],[J.ENUM_TYPE_EXTENSION]:["EnumDef"],[J.INPUT_OBJECT_TYPE_EXTENSION]:["InputDef"]};var kc;function _c(e){return{style:"keyword",match:t=>"Name"===t.kind&&t.value===e}}function Nc(e){return{style:e,match:e=>"Name"===e.kind,update(e,t){e.name=t.value}}}function Dc(e={eatWhitespace:e=>e.eatWhile(Tc),lexRules:Cc,parseRules:Sc,editorConfig:{}}){return{startState(){const t={level:0,step:0,name:null,kind:null,type:null,rule:null,needsSeparator:!1,prevState:null};return Oc(e.parseRules,t,J.DOCUMENT),t},token:(t,n)=>function(e,t,n){var r;if(t.inBlockstring)return e.match(/.*"""/)?(t.inBlockstring=!1,"string"):(e.skipToEnd(),"string");const{lexRules:i,parseRules:o,eatWhitespace:s,editorConfig:a}=n;t.rule&&0===t.rule.length?Lc(t):t.needsAdvance&&(t.needsAdvance=!1,Mc(t,!0));if(e.sol()){const n=(null==a?void 0:a.tabSize)||2;t.indentLevel=Math.floor(e.indentation()/n)}if(s(e))return"ws";const l=function(e,t){const n=Object.keys(e);for(let r=0;r0&&e.at(-1){let t=jc.UNKNOWN;if(e)try{at(je(e),{enter(e){if("Document"!==e.kind)return!!Bc.includes(e.kind)&&(t=jc.TYPE_SYSTEM,st);t=jc.EXECUTABLE}})}catch(n){return t}return t};function Uc(e,t,n,r,i){const o=r||function(e,t,n=0){let r=null,i=null,o=null;const s=Pc(e,((e,s,a,l)=>{if(!(l!==t.line||e.getCurrentPosition()+n{var p;switch(t.kind){case Wc.QUERY:case"ShortQuery":d=e.getQueryType();break;case Wc.MUTATION:d=e.getMutationType();break;case Wc.SUBSCRIPTION:d=e.getSubscriptionType();break;case Wc.INLINE_FRAGMENT:case Wc.FRAGMENT_DEFINITION:t.type&&(d=e.getType(t.type));break;case Wc.FIELD:case Wc.ALIASED_FIELD:d&&t.name?(s=u?Hc(e,u,t.name):null,d=s?s.type:null):s=null;break;case Wc.SELECTION_SET:u=qt(d);break;case Wc.DIRECTIVE:i=t.name?e.getDirective(t.name):null;break;case Wc.INTERFACE_DEF:t.name&&(l=null,f=new nn({name:t.name,interfaces:[],fields:{}}));break;case Wc.OBJECT_TYPE_DEF:t.name&&(f=null,l=new Kt({name:t.name,interfaces:[],fields:{}}));break;case Wc.ARGUMENTS:if(t.prevState)switch(t.prevState.kind){case Wc.FIELD:r=s&&s.args;break;case Wc.DIRECTIVE:r=i&&i.args;break;case Wc.ALIASED_FIELD:{const n=null===(p=t.prevState)||void 0===p?void 0:p.name;if(!n){r=null;break}const i=u?Hc(e,u,n):null;if(!i){r=null;break}r=i.args;break}default:r=null}else r=null;break;case Wc.ARGUMENT:if(r)for(let e=0;ee.value===t.name)):null;break;case Wc.LIST_VALUE:const m=Ut(a);a=m instanceof Pt?m.ofType:null;break;case Wc.OBJECT_VALUE:const g=qt(a);c=g instanceof cn?g.getFields():null;break;case Wc.OBJECT_FIELD:const v=t.name&&c?c[t.name]:null;a=null==v?void 0:v.type,s=v,d=s?s.type:null;break;case Wc.NAMED_TYPE:t.name&&(d=e.getType(t.name))}})),{argDef:n,argDefs:r,directiveDef:i,enumValue:o,fieldDef:s,inputType:a,objectFieldDefs:c,parentType:u,type:d,interfaceDef:f,objectTypeDef:l}}(n,o.state);var l,c;return{token:o,state:s,typeInfo:a,mode:(null==i?void 0:i.mode)||(l=e,(null==(c=null==i?void 0:i.uri)?void 0:c.endsWith(".graphqls"))?jc.TYPE_SYSTEM:$c(l))}}function Hc(e,t,n){return n===Kn.name&&e.getQueryType()===t?Kn:n===Yn.name&&e.getQueryType()===t?Yn:n===Qn.name&&Mt(t)?Qn:"getFields"in t?t.getFields()[n]:null}function qc(e,t){const n=[];let r=e;for(;null==r?void 0:r.kind;)n.push(r),r=r.prevState;for(let i=n.length-1;i>=0;i--)t(n[i])}const Wc=Object.assign(Object.assign({},J),{ALIASED_FIELD:"AliasedField",ARGUMENTS:"Arguments",SHORT_QUERY:"ShortQuery",QUERY:"Query",MUTATION:"Mutation",SUBSCRIPTION:"Subscription",TYPE_CONDITION:"TypeCondition",INVALID:"Invalid",COMMENT:"Comment",SCHEMA_DEF:"SchemaDef",SCALAR_DEF:"ScalarDef",OBJECT_TYPE_DEF:"ObjectTypeDef",OBJECT_VALUE:"ObjectValue",LIST_VALUE:"ListValue",INTERFACE_DEF:"InterfaceDef",UNION_DEF:"UnionDef",ENUM_DEF:"EnumDef",ENUM_VALUE:"EnumValue",FIELD_DEF:"FieldDef",INPUT_DEF:"InputDef",INPUT_VALUE_DEF:"InputValueDef",ARGUMENTS_DEF:"ArgumentsDef",EXTEND_DEF:"ExtendDef",EXTENSION_DEFINITION:"ExtensionDefinition",DIRECTIVE_DEF:"DirectiveDef",IMPLEMENTS:"Implements",VARIABLE_DEFINITIONS:"VariableDefinitions",TYPE:"Type",VARIABLE:"Variable"});var zc;!function(e){e.Text=1,e.Method=2,e.Function=3,e.Constructor=4,e.Field=5,e.Variable=6,e.Class=7,e.Interface=8,e.Module=9,e.Property=10,e.Unit=11,e.Value=12,e.Enum=13,e.Keyword=14,e.Snippet=15,e.Color=16,e.File=17,e.Reference=18,e.Folder=19,e.EnumMember=20,e.Constant=21,e.Struct=22,e.Event=23,e.Operator=24,e.TypeParameter=25}(zc||(zc={}));const Gc={command:"editor.action.triggerSuggest",title:"Suggestions"};function Kc(e,t,n,r,i,o){var s;const a=Object.assign(Object.assign({},o),{schema:e}),l=Uc(t,n,e,r,o);if(!l)return[];const{state:c,typeInfo:u,mode:d,token:f}=l,{kind:p,step:h,prevState:m}=c;if(p===Wc.DOCUMENT)return d===jc.TYPE_SYSTEM?function(e){return $s(e,[{label:"extend",kind:zc.Function},...Yc])}(f):d===jc.EXECUTABLE?function(e){return $s(e,Qc)}(f):function(e){return $s(e,[{label:"extend",kind:zc.Function},...Qc,...Yc])}(f);if(p===Wc.EXTEND_DEF)return function(e){return $s(e,Yc)}(f);if((null===(s=null==m?void 0:m.prevState)||void 0===s?void 0:s.kind)===Wc.EXTENSION_DEFINITION&&c.name)return $s(f,[]);if((null==m?void 0:m.kind)===J.SCALAR_TYPE_EXTENSION)return $s(f,Object.values(e.getTypeMap()).filter(xt).map((e=>({label:e.name,kind:zc.Function}))));if((null==m?void 0:m.kind)===J.OBJECT_TYPE_EXTENSION)return $s(f,Object.values(e.getTypeMap()).filter((e=>wt(e)&&!e.name.startsWith("__"))).map((e=>({label:e.name,kind:zc.Function}))));if((null==m?void 0:m.kind)===J.INTERFACE_TYPE_EXTENSION)return $s(f,Object.values(e.getTypeMap()).filter(Ct).map((e=>({label:e.name,kind:zc.Function}))));if((null==m?void 0:m.kind)===J.UNION_TYPE_EXTENSION)return $s(f,Object.values(e.getTypeMap()).filter(kt).map((e=>({label:e.name,kind:zc.Function}))));if((null==m?void 0:m.kind)===J.ENUM_TYPE_EXTENSION)return $s(f,Object.values(e.getTypeMap()).filter((e=>_t(e)&&!e.name.startsWith("__"))).map((e=>({label:e.name,kind:zc.Function}))));if((null==m?void 0:m.kind)===J.INPUT_OBJECT_TYPE_EXTENSION)return $s(f,Object.values(e.getTypeMap()).filter(Nt).map((e=>({label:e.name,kind:zc.Function}))));if(p===Wc.IMPLEMENTS||p===Wc.NAMED_TYPE&&(null==m?void 0:m.kind)===Wc.IMPLEMENTS)return function(e,t,n,r,i){if(t.needsSeparator)return[];const o=n.getTypeMap(),s=Bs(o).filter(Ct),a=s.map((({name:e})=>e)),l=new Set;Pc(r,((e,t)=>{var r,o,s,c,u;if(t.name&&(t.kind!==Wc.INTERFACE_DEF||a.includes(t.name)||l.add(t.name),t.kind===Wc.NAMED_TYPE&&(null===(r=t.prevState)||void 0===r?void 0:r.kind)===Wc.IMPLEMENTS))if(i.interfaceDef){if(null===(o=i.interfaceDef)||void 0===o?void 0:o.getInterfaces().find((({name:e})=>e===t.name)))return;const e=n.getType(t.name),r=null===(s=i.interfaceDef)||void 0===s?void 0:s.toConfig();i.interfaceDef=new nn(Object.assign(Object.assign({},r),{interfaces:[...r.interfaces,e||new nn({name:t.name,fields:{}})]}))}else if(i.objectTypeDef){if(null===(c=i.objectTypeDef)||void 0===c?void 0:c.getInterfaces().find((({name:e})=>e===t.name)))return;const e=n.getType(t.name),r=null===(u=i.objectTypeDef)||void 0===u?void 0:u.toConfig();i.objectTypeDef=new Kt(Object.assign(Object.assign({},r),{interfaces:[...r.interfaces,e||new nn({name:t.name,fields:{}})]}))}}));const c=i.interfaceDef||i.objectTypeDef,u=((null==c?void 0:c.getInterfaces())||[]).map((({name:e})=>e)),d=s.concat([...l].map((e=>({name:e})))).filter((({name:e})=>e!==(null==c?void 0:c.name)&&!u.includes(e)));return $s(e,d.map((e=>{const t={label:e.name,kind:zc.Interface,type:e};return(null==e?void 0:e.description)&&(t.documentation=e.description),t})))}(f,c,e,t,u);if(p===Wc.SELECTION_SET||p===Wc.FIELD||p===Wc.ALIASED_FIELD)return function(e,t,n){var r;if(t.parentType){const{parentType:i}=t;let o=[];return"getFields"in i&&(o=Bs(i.getFields())),Mt(i)&&o.push(Qn),i===(null===(r=null==n?void 0:n.schema)||void 0===r?void 0:r.getQueryType())&&o.push(Kn,Yn),$s(e,o.map(((t,r)=>{var i;const o={sortText:String(r)+t.name,label:t.name,detail:String(t.type),documentation:null!==(i=t.description)&&void 0!==i?i:void 0,deprecated:Boolean(t.deprecationReason),isDeprecated:Boolean(t.deprecationReason),deprecationReason:t.deprecationReason,kind:zc.Field,labelDetails:{detail:" "+t.type.toString()},type:t.type};return(null==n?void 0:n.fillLeafsOnComplete)&&(o.insertText=Gs(t),o.insertText||(o.insertText=Ws(t.name,t.type,t.name+(e.state.needsAdvance?"":"\n"))),o.insertText&&(o.insertTextFormat=il.Snippet,o.insertTextMode=cl.adjustIndentation,o.command=Gc)),o})))}return[]}(f,u,a);if(p===Wc.ARGUMENTS||p===Wc.ARGUMENT&&0===h){const{argDefs:e}=u;if(e)return $s(f,e.map((e=>{var t;return{label:e.name,insertText:zs(e.name+": ",e.type),insertTextMode:cl.adjustIndentation,insertTextFormat:il.Snippet,command:Gc,labelDetails:{detail:" "+String(e.type)},documentation:null!==(t=e.description)&&void 0!==t?t:void 0,kind:zc.Variable,type:e.type}})))}if((p===Wc.OBJECT_VALUE||p===Wc.OBJECT_FIELD&&0===h)&&u.objectFieldDefs){const e=Bs(u.objectFieldDefs),t=p===Wc.OBJECT_VALUE?zc.Value:zc.Field;return $s(f,e.map((e=>{var n;return{label:e.name,detail:String(e.type),documentation:null!==(n=null==e?void 0:e.description)&&void 0!==n?n:void 0,kind:t,type:e.type,insertText:zs(e.name+": ",e.type),insertTextMode:cl.adjustIndentation,insertTextFormat:il.Snippet,command:Gc}})))}if(p===Wc.ENUM_VALUE||p===Wc.LIST_VALUE&&1===h||p===Wc.OBJECT_FIELD&&2===h||p===Wc.ARGUMENT&&2===h)return function(e,t,n,r){const i=qt(t.inputType),o=Jc(n,r,e).filter((e=>e.detail===(null==i?void 0:i.name)));if(i instanceof sn){return $s(e,i.getValues().map((e=>{var t;return{label:e.name,detail:String(i),documentation:null!==(t=e.description)&&void 0!==t?t:void 0,deprecated:Boolean(e.deprecationReason),isDeprecated:Boolean(e.deprecationReason),deprecationReason:e.deprecationReason,kind:zc.EnumMember,type:i}})).concat(o))}if(i===En)return $s(e,o.concat([{label:"true",detail:String(En),documentation:"Not false.",kind:zc.Variable,type:En},{label:"false",detail:String(En),documentation:"Not true.",kind:zc.Variable,type:En}]));return o}(f,u,t,e);if(p===Wc.VARIABLE&&1===h){const n=qt(u.inputType);return $s(f,Jc(t,e,f).filter((e=>e.detail===(null==n?void 0:n.name))))}if(p===Wc.TYPE_CONDITION&&1===h||p===Wc.NAMED_TYPE&&null!=m&&m.kind===Wc.TYPE_CONDITION)return function(e,t,n,r){let i;if(t.parentType)if(Rt(t.parentType)){const e=Ft(t.parentType),r=n.getPossibleTypes(e),o=Object.create(null);for(const t of r)for(const e of t.getInterfaces())o[e.name]=e;i=r.concat(Bs(o))}else i=[t.parentType];else{i=Bs(n.getTypeMap()).filter((e=>Mt(e)&&!e.name.startsWith("__")))}return $s(e,i.map((e=>{const t=qt(e);return{label:String(e),documentation:(null==t?void 0:t.description)||"",kind:zc.Field}})))}(f,u,e);if(p===Wc.FRAGMENT_SPREAD&&1===h)return function(e,t,n,r,i){if(!r)return[];const o=n.getTypeMap(),s=function(e){let t;return qc(e,(e=>{switch(e.kind){case"Query":case"ShortQuery":case"Mutation":case"Subscription":case"FragmentDefinition":t=e}})),t}(e.state),a=function(e){const t=[];return Pc(e,((e,n)=>{n.kind===Wc.FRAGMENT_DEFINITION&&n.name&&n.type&&t.push({kind:Wc.FRAGMENT_DEFINITION,name:{kind:J.NAME,value:n.name},selectionSet:{kind:Wc.SELECTION_SET,selections:[]},typeCondition:{kind:Wc.NAMED_TYPE,name:{kind:J.NAME,value:n.type}}})})),t}(r);i&&i.length>0&&a.push(...i);const l=a.filter((e=>o[e.typeCondition.name.value]&&!(s&&s.kind===Wc.FRAGMENT_DEFINITION&&s.name===e.name.value)&&Mt(t.parentType)&&Mt(o[e.typeCondition.name.value])&&hn(n,t.parentType,o[e.typeCondition.name.value])));return $s(e,l.map((e=>({label:e.name.value,detail:String(o[e.typeCondition.name.value]),documentation:`fragment ${e.name.value} on ${e.typeCondition.name.value}`,labelDetails:{detail:`fragment ${e.name.value} on ${e.typeCondition.name.value}`},kind:zc.Field,type:o[e.typeCondition.name.value]}))))}(f,u,e,t,Array.isArray(i)?i:(e=>{const t=[];if(e)try{at(je(e),{FragmentDefinition(e){t.push(e)}})}catch(s){return[]}return t})(i));const g=Zc(c);return g.kind===Wc.FIELD_DEF?$s(f,Object.values(e.getTypeMap()).filter((e=>Ot(e)&&!e.name.startsWith("__"))).map((e=>({label:e.name,kind:zc.Function,insertText:(null==o?void 0:o.fillLeafsOnComplete)?e.name+"\n":e.name,insertTextMode:cl.adjustIndentation})))):g.kind===Wc.INPUT_VALUE_DEF&&2===h?$s(f,Object.values(e.getTypeMap()).filter((e=>It(e)&&!e.name.startsWith("__"))).map((e=>({label:e.name,kind:zc.Function,insertText:(null==o?void 0:o.fillLeafsOnComplete)?e.name+"\n$1":e.name,insertTextMode:cl.adjustIndentation,insertTextFormat:il.Snippet})))):p===Wc.VARIABLE_DEFINITION&&2===h||p===Wc.LIST_TYPE&&1===h||p===Wc.NAMED_TYPE&&m&&(m.kind===Wc.VARIABLE_DEFINITION||m.kind===Wc.LIST_TYPE||m.kind===Wc.NON_NULL_TYPE)?function(e,t,n){const r=t.getTypeMap(),i=Bs(r).filter(It);return $s(e,i.map((e=>({label:e.name,documentation:(null==e?void 0:e.description)||"",kind:zc.Variable}))))}(f,e):p===Wc.DIRECTIVE?function(e,t,n,r){var i;if(null===(i=t.prevState)||void 0===i?void 0:i.kind){const r=n.getDirectives().filter((e=>function(e,t){if(!(null==e?void 0:e.kind))return!1;const{kind:n,prevState:r}=e,{locations:i}=t;switch(n){case Wc.QUERY:return i.includes(Q.QUERY);case Wc.MUTATION:return i.includes(Q.MUTATION);case Wc.SUBSCRIPTION:return i.includes(Q.SUBSCRIPTION);case Wc.FIELD:case Wc.ALIASED_FIELD:return i.includes(Q.FIELD);case Wc.FRAGMENT_DEFINITION:return i.includes(Q.FRAGMENT_DEFINITION);case Wc.FRAGMENT_SPREAD:return i.includes(Q.FRAGMENT_SPREAD);case Wc.INLINE_FRAGMENT:return i.includes(Q.INLINE_FRAGMENT);case Wc.SCHEMA_DEF:return i.includes(Q.SCHEMA);case Wc.SCALAR_DEF:return i.includes(Q.SCALAR);case Wc.OBJECT_TYPE_DEF:return i.includes(Q.OBJECT);case Wc.FIELD_DEF:return i.includes(Q.FIELD_DEFINITION);case Wc.INTERFACE_DEF:return i.includes(Q.INTERFACE);case Wc.UNION_DEF:return i.includes(Q.UNION);case Wc.ENUM_DEF:return i.includes(Q.ENUM);case Wc.ENUM_VALUE:return i.includes(Q.ENUM_VALUE);case Wc.INPUT_DEF:return i.includes(Q.INPUT_OBJECT);case Wc.INPUT_VALUE_DEF:switch(null==r?void 0:r.kind){case Wc.ARGUMENTS_DEF:return i.includes(Q.ARGUMENT_DEFINITION);case Wc.INPUT_DEF:return i.includes(Q.INPUT_FIELD_DEFINITION)}}return!1}(t.prevState,e)));return $s(e,r.map((e=>({label:e.name,documentation:(null==e?void 0:e.description)||"",kind:zc.Function}))))}return[]}(f,c,e):p===Wc.DIRECTIVE_DEF?function(e,t,n,r){const i=n.getDirectives().find((e=>e.name===t.name));return $s(e,(null==i?void 0:i.args.map((e=>({label:e.name,documentation:e.description||"",kind:zc.Field}))))||[])}(f,c,e):[]}const Yc=[{label:"type",kind:zc.Function},{label:"interface",kind:zc.Function},{label:"union",kind:zc.Function},{label:"input",kind:zc.Function},{label:"scalar",kind:zc.Function},{label:"schema",kind:zc.Function}],Qc=[{label:"query",kind:zc.Function},{label:"mutation",kind:zc.Function},{label:"subscription",kind:zc.Function},{label:"fragment",kind:zc.Function},{label:"{",kind:zc.Constructor}];const Xc=(e,t)=>{var n,r,i,o,s,a,l,c,u,d;return(null===(n=e.prevState)||void 0===n?void 0:n.kind)===t?e.prevState:(null===(i=null===(r=e.prevState)||void 0===r?void 0:r.prevState)||void 0===i?void 0:i.kind)===t?e.prevState.prevState:(null===(a=null===(s=null===(o=e.prevState)||void 0===o?void 0:o.prevState)||void 0===s?void 0:s.prevState)||void 0===a?void 0:a.kind)===t?e.prevState.prevState.prevState:(null===(d=null===(u=null===(c=null===(l=e.prevState)||void 0===l?void 0:l.prevState)||void 0===c?void 0:c.prevState)||void 0===u?void 0:u.prevState)||void 0===d?void 0:d.kind)===t?e.prevState.prevState.prevState.prevState:void 0};function Jc(e,t,n){let r,i=null;const o=Object.create({});return Pc(e,((e,s)=>{var a;if((null==s?void 0:s.kind)===Wc.VARIABLE&&s.name&&(i=s.name),(null==s?void 0:s.kind)===Wc.NAMED_TYPE&&i){const e=Xc(s,Wc.TYPE);(null==e?void 0:e.type)&&(r=t.getType(null==e?void 0:e.type))}if(i&&r&&!o[i]){const e="$"===n.string||"Variable"===(null===(a=null==n?void 0:n.state)||void 0===a?void 0:a.kind)?i:"$"+i;o[i]={detail:r.toString(),insertText:e,label:"$"+i,rawInsert:e,type:r,kind:zc.Variable},i=null,r=null}})),Bs(o)}function Zc(e){return e.prevState&&e.kind&&[Wc.NAMED_TYPE,Wc.LIST_TYPE,Wc.TYPE,Wc.NON_NULL_TYPE].includes(e.kind)?Zc(e.prevState):e}var eu,tu={exports:{}};const nu=s(function(){if(eu)return tu.exports;function e(e,t){if(null!=e)return e;var n=new Error(void 0!==t?t:"Got unexpected "+e);throw n.framesToPop=1,n}return eu=1,tu.exports=e,tu.exports.default=e,Object.defineProperty(tu.exports,"__esModule",{value:!0}),tu.exports}());class ru{constructor(e,t){this.containsPosition=e=>this.start.line===e.line?this.start.character<=e.character:this.end.line===e.line?this.end.character>=e.character:this.start.line<=e.line&&this.end.line>=e.line,this.start=e,this.end=t}setStart(e,t){this.start=new iu(e,t)}setEnd(e,t){this.end=new iu(e,t)}}class iu{constructor(e,t){this.lessThanOrEqualTo=e=>this.line{if(!e)throw new Error(t)};function lu(e,t=null,n,r,i){var o,s;let a=null,l="";i&&(l="string"==typeof i?i:i.reduce(((e,t)=>e+ut(t)+"\n\n"),""));const c=l?`${e}\n\n${l}`:e;try{a=je(c)}catch(u){if(u instanceof B){const e=function(e,t){const n=Dc(),r=n.startState(),i=t.split("\n");au(i.length>=e.line,"Query text must have more lines than where the error happened");let o=null;for(let c=0;ce!==Hr&&e!==Dr));return n&&Array.prototype.push.apply(o,n),eo(e,t,o).filter((e=>{if(e.message.includes("Unknown directive")&&e.nodes){const t=e.nodes[0];if(t&&t.kind===J.DIRECTIVE){const e=t.name.value;if("arguments"===e||"argumentDefinitions"===e)return!1}}return!0}))}(t,e,n).flatMap((e=>cu(e,su.Error,"Validation"))),o=eo(t,e,[ko]).flatMap((e=>cu(e,su.Warning,"Deprecation")));return i.concat(o)}(a,t,n)}function cu(e,t,n){if(!e.nodes)return[];const r=[];for(const[i,o]of e.nodes.entries()){const s="Variable"!==o.kind&&"name"in o&&void 0!==o.name?o.name:"variable"in o&&void 0!==o.variable?o.variable:o;if(s){au(e.locations,"GraphQL validation error requires locations.");const o=e.locations[i],a=uu(s),l=o.column+(a.end-a.start);r.push({source:`GraphQL: ${n}`,message:e.message,severity:t,range:new ru(new iu(o.line-1,o.column-1),new iu(o.line-1,l))})}}return r}function uu(e){const t=e.loc;return au(t,"Expected ASTNode to have a location."),t} +/*! + * is-primitive + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */var du,fu,pu,hu,mu,gu,vu,yu;function bu(){if(gu)return mu;gu=1;var e=hu?pu:(hu=1,pu=function(e){return null!=e&&"object"==typeof e&&!1===Array.isArray(e)});function t(t){return!0===e(t)&&"[object Object]"===Object.prototype.toString.call(t)}return mu=function(e){var n,r;return!1!==t(e)&&("function"==typeof(n=e.constructor)&&(!1!==t(r=n.prototype)&&!1!==r.hasOwnProperty("isPrototypeOf")))}} +/*! + * set-value + * + * Copyright (c) Jon Schlinkert (https://github.com/jonschlinkert). + * Released under the MIT License. + */var Eu=function(){if(yu)return vu;yu=1;const{deleteProperty:e}=Reflect,t=fu?du:(fu=1,du=function(e){return"object"==typeof e?null===e:"function"!=typeof e}),n=bu(),r=e=>"object"==typeof e&&null!==e||"function"==typeof e,i=e=>{if(!t(e))throw new TypeError("Object keys must be strings or symbols");if((e=>"__proto__"===e||"constructor"===e||"prototype"===e)(e))throw new Error(`Cannot set unsafe key: "${e}"`)},o=(e,t,n)=>{const r=(e=>Array.isArray(e)?e.flat().map(String).join(","):e)(t?((e,t)=>{if("string"!=typeof e||!t)return e;let n=e+";";return void 0!==t.arrays&&(n+=`arrays=${t.arrays};`),void 0!==t.separator&&(n+=`separator=${t.separator};`),void 0!==t.split&&(n+=`split=${t.split};`),void 0!==t.merge&&(n+=`merge=${t.merge};`),void 0!==t.preservePaths&&(n+=`preservePaths=${t.preservePaths};`),n})(e,t):e);i(r);const o=l.cache.get(r)||n();return l.cache.set(r,o),o},s=(e,t)=>t&&"function"==typeof t.split?t.split(e):"symbol"==typeof e?[e]:Array.isArray(e)?e:o(e,t,(()=>((e,t={})=>{const n=t.separator||".",r="/"!==n&&t.preservePaths;if("string"==typeof e&&!1!==r&&/\//.test(e))return[e];const i=[];let o="";const s=e=>{let t;""!==e.trim()&&Number.isInteger(t=Number(e))?i.push(t):i.push(e)};for(let a=0;a{if(i(r),void 0===o)e(t,r);else if(s&&s.merge){const e="function"===s.merge?s.merge:Object.assign;e&&n(t[r])&&n(o)?t[r]=e(t[r],o):t[r]=o}else t[r]=o;return t},l=(e,t,n,o)=>{if(!t||!r(e))return e;const l=s(t,o);let c=e;for(let s=0;s{l.cache=new Map},vu=l}();const xu=s(Eu); +/*! + * isobject + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */var wu,Tu,Cu,Su;var ku=function(){if(Su)return Cu;Su=1;const e=Tu?wu:(Tu=1,wu=function(e){return null!=e&&"object"==typeof e&&!1===Array.isArray(e)});function t(e,t,n){return"function"==typeof n.join?n.join(e):e[0]+t+e[1]}function n(e,t,n){return"function"!=typeof n.isValid||n.isValid(e,t)}function r(t){return e(t)||Array.isArray(t)||"function"==typeof t}return Cu=function(i,o,s){if(e(s)||(s={default:s}),!r(i))return void 0!==s.default?s.default:i;"number"==typeof o&&(o=String(o));const a=Array.isArray(o),l="string"==typeof o,c=s.separator||".",u=s.joinChar||("string"==typeof c?c:".");if(!l&&!a)return i;if(l&&o in i)return n(o,i,s)?i[o]:s.default;let d=a?o:function(e,t,n){if("function"==typeof n.split)return n.split(e);return e.split(t)}(o,c,s),f=d.length,p=0;do{let e=d[p];for("number"==typeof e&&(e=String(e));e&&"\\"===e.slice(-1);)e=t([e.slice(0,-1),d[++p]||""],u,s);if(e in i){if(!n(e,i,s))return s.default;i=i[e]}else{let r=!1,o=p+1;for(;o{let t;const n=new Set,r=(e,r)=>{const i="function"==typeof e?e(t):e;if(!Object.is(i,t)){const e=t;t=(null!=r?r:"object"!=typeof i||null===i)?i:Object.assign({},t,i),n.forEach((n=>n(t,e)))}},i=()=>t,o={setState:r,getState:i,getInitialState:()=>s,subscribe:e=>(n.add(e),()=>n.delete(e))},s=t=e(r,i,o);return o},Iu=e=>e?Au(e):Au,Ou=e=>e;const Lu=t=>n=>function(t,n=Ou){const r=e.useSyncExternalStore(t.subscribe,(()=>n(t.getState())),(()=>n(t.getInitialState())));return e.useDebugValue(r),r}(t,n),Mu=Iu((()=>({storage:null}))),Ru=t=>{const n=h.c(3),{storage:r,children:i}=t,o=Fu(ju);let s,a;return n[0]!==r?(s=()=>{Mu.setState({storage:new Fs(r)})},a=[r],n[0]=r,n[1]=s,n[2]=a):(s=n[1],a=n[2]),e.useEffect(s,a),o?i:null},Fu=Lu(Mu),Pu=()=>Fu(Vu);function ju(e){return Boolean(e.storage)}function Vu(e){return e.storage}const Bu="undefined"!=typeof navigator&&navigator.userAgent.includes("Mac"),$u="graphiql",Uu="sublime",Hu={[Bu?"Cmd-F":"Ctrl-F"]:"findPersistent","Cmd-G":"findPersistent","Ctrl-G":"findPersistent","Ctrl-Left":"goSubwordLeft","Ctrl-Right":"goSubwordRight","Alt-Left":"goGroupLeft","Alt-Right":"goGroupRight"};async function qu(e,t){const n=await Promise.resolve().then((()=>Wj)).then((e=>"function"==typeof e?e:e.default));return await Promise.all(!1===(null==t?void 0:t.useCommonAddons)?e:[Promise.resolve().then((()=>Yj)),Promise.resolve().then((()=>eV)),Promise.resolve().then((()=>iV)),Promise.resolve().then((()=>lV)),Promise.resolve().then((()=>mV)),Promise.resolve().then((()=>bV)),Promise.resolve().then((()=>CV)),Promise.resolve().then((()=>IV)),Promise.resolve().then((()=>LV)),Promise.resolve().then((()=>PV)),...e]),n}var Wu,zu,Gu,Ku;var Yu=function(){if(Ku)return Gu;Ku=1;var e=zu?Wu:(zu=1,Wu=function(){var e=document.getSelection();if(!e.rangeCount)return function(){};for(var t=document.activeElement,n=[],r=0;r({plugins:[],visiblePlugin:null,referencePlugin:void 0,setVisiblePlugin(n){const{plugins:r,onTogglePluginVisibility:i}=t(),o="string"==typeof n,s=n&&r.find((e=>(o?e.title:e)===n))||null;e((({visiblePlugin:e})=>s===e?{visiblePlugin:e}:(null==i||i(s),{visiblePlugin:s})))}}))),Ju=t=>{const n=h.c(8),{onTogglePluginVisibility:r,children:i,visiblePlugin:o,plugins:s,referencePlugin:a}=t;let l;n[0]!==s?(l=void 0===s?[]:s,n[0]=s,n[1]=l):l=n[1];const c=l;let u,d;return n[2]!==r||n[3]!==c||n[4]!==a||n[5]!==o?(u=()=>{const e=new Set;for(const{title:t}of c){if("string"!=typeof t||!t)throw new Error("All GraphiQL plugins must have a unique title");if(e.has(t))throw new Error(`All GraphiQL plugins must have a unique title, found two plugins with the title '${t}'`);e.add(t)}Xu.setState({plugins:c,onTogglePluginVisibility:r,referencePlugin:a}),Xu.getState().setVisiblePlugin(o??null)},d=[c,r,a,o],n[2]=r,n[3]=c,n[4]=a,n[5]=o,n[6]=u,n[7]=d):(u=n[6],d=n[7]),e.useEffect(u,d),i},Zu=Lu(Xu),ed=Iu(((e,t)=>({inputValueDeprecation:null,introspectionQueryName:null,schemaDescription:null,fetcher:null,onSchemaChange:void 0,fetchError:null,isFetching:!1,schema:null,validationErrors:[],schemaReference:null,setSchemaReference(t){e({schemaReference:t})},requestCounter:0,shouldIntrospect:!0,async introspect(){const{requestCounter:n,fetcher:r,onSchemaChange:i,shouldIntrospect:o,headerEditor:s,...a}=t();if(!o)return;const l=n+1;e({requestCounter:l});try{const n=function(e){let t=null,n=!0;try{e&&(t=JSON.parse(e))}catch{n=!1}return{headers:t,isValidJSON:n}}(null==s?void 0:s.getValue());if(!n.isValidJSON)return void e({fetchError:"Introspection failed as headers are invalid."});const o=n.headers?{headers:n.headers}:{},{introspectionQuery:c,introspectionQueryName:u,introspectionQuerySansSubscriptions:d}=function({inputValueDeprecation:e,introspectionQueryName:t,schemaDescription:n}){const r=_o({inputValueDeprecation:e,schemaDescription:n}),i="IntrospectionQuery"===t?r:r.replace("query IntrospectionQuery",`query ${t}`),o=r.replace("subscriptionType { name }","");return{introspectionQueryName:t,introspectionQuery:i,introspectionQuerySansSubscriptions:o}}(a),f=D(r({query:c,operationName:u},o));if(!k(f))return void e({fetchError:"Fetcher did not return a Promise for introspection."});e({isFetching:!0,fetchError:null});let p,h=await f;if("object"!=typeof h||null===h||!("data"in h)){const e=D(r({query:d,operationName:u},o));if(!k(e))throw new Error("Fetcher did not return a Promise for introspection.");h=await e}if(e({isFetching:!1}),(null==h?void 0:h.data)&&"__schema"in h.data)p=h.data;else{const t="string"==typeof h?h:Ds(h);e({fetchError:t})}if(l!==t().requestCounter||!p)return;const m=No(p);e({schema:m}),null==i||i(m)}catch(c){if(l!==t().requestCounter)return;e({fetchError:Ns(c),isFetching:!1})}}}))),td=t=>{const n=h.c(14),{fetcher:r,onSchemaChange:i,dangerouslyAssumeSchemaIsValid:o,children:s,schema:a,inputValueDeprecation:l,introspectionQueryName:c,schemaDescription:u}=t,d=void 0!==o&&o,f=void 0!==l&&l,p=void 0===c?"IntrospectionQuery":c,m=void 0!==u&&u;if(!r)throw new TypeError("The `SchemaContextProvider` component requires a `fetcher` function to be passed as prop.");let g;n[0]===Symbol.for("react.memo_cache_sentinel")?(g={nonNull:!0,caller:td},n[0]=g):g=n[0];const{headerEditor:v}=Qh(g);let y,b,E,x,w;return n[1]!==v?(y=()=>{v&&ed.setState({headerEditor:v})},b=[v],n[1]=v,n[2]=y,n[3]=b):(y=n[2],b=n[3]),e.useEffect(y,b),n[4]!==d||n[5]!==r||n[6]!==f||n[7]!==p||n[8]!==i||n[9]!==a||n[10]!==m?(E=()=>{const e=Zn(a)||null==a?a:void 0,t=!e||d?[]:rr(e);ed.setState((n=>{const{requestCounter:o}=n;return{fetcher:r,onSchemaChange:i,schema:e,shouldIntrospect:!Zn(a)&&null!==a,inputValueDeprecation:f,introspectionQueryName:p,schemaDescription:m,validationErrors:t,requestCounter:o+1}})),ed.getState().introspect()},x=[a,d,i,r,f,p,m],n[4]=d,n[5]=r,n[6]=f,n[7]=p,n[8]=i,n[9]=a,n[10]=m,n[11]=E,n[12]=x):(E=n[11],x=n[12]),e.useEffect(E,x),n[13]===Symbol.for("react.memo_cache_sentinel")?(w=[],n[13]=w):w=n[13],e.useEffect(rd,w),s},nd=Lu(ed);function rd(){const e=function(e){e.ctrlKey&&"R"===e.key&&ed.getState().introspect()};return window.addEventListener("keydown",e),()=>{window.removeEventListener("keydown",e)}}const id={};function od(e,t){"string"!=typeof t&&(t=od.defaultChars);const n=function(e){let t=id[e];if(t)return t;t=id[e]=[];for(let n=0;n<128;n++){const e=String.fromCharCode(n);t.push(e)}for(let n=0;n=55296&&e<=57343?"���":String.fromCharCode(e),r+=6;continue}}if(240==(248&o)&&r+91114111?t+="����":(e-=65536,t+=String.fromCharCode(55296+(e>>10),56320+(1023&e))),r+=9;continue}}t+="�"}}return t}))}od.defaultChars=";/?:@&=+$,#",od.componentChars="";const sd={};function ad(e,t,n){"string"!=typeof t&&(n=t,t=ad.defaultChars),void 0===n&&(n=!0);const r=function(e){let t=sd[e];if(t)return t;t=sd[e]=[];for(let n=0;n<128;n++){const e=String.fromCharCode(n);/^[0-9a-z]$/i.test(e)?t.push(e):t.push("%"+("0"+n.toString(16).toUpperCase()).slice(-2))}for(let n=0;n=55296&&t<=57343){if(t>=55296&&t<=56319&&o+1=56320&&t<=57343){i+=encodeURIComponent(e[o]+e[o+1]),o++;continue}}i+="%EF%BF%BD"}else i+=encodeURIComponent(e[o])}return i}function ld(e){let t="";return t+=e.protocol||"",t+=e.slashes?"//":"",t+=e.auth?e.auth+"@":"",e.hostname&&-1!==e.hostname.indexOf(":")?t+="["+e.hostname+"]":t+=e.hostname||"",t+=e.port?":"+e.port:"",t+=e.pathname||"",t+=e.search||"",t+=e.hash||"",t}function cd(){this.protocol=null,this.slashes=null,this.auth=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.pathname=null}ad.defaultChars=";/?:@&=+$,-_.!~*'()#",ad.componentChars="-_.!~*'()";const ud=/^([a-z0-9.+-]+:)/i,dd=/:[0-9]*$/,fd=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,pd=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),hd=["'"].concat(pd),md=["%","/","?",";","#"].concat(hd),gd=["/","?","#"],vd=/^[+a-z0-9A-Z_-]{0,63}$/,yd=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,bd={javascript:!0,"javascript:":!0},Ed={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0};function xd(e,t){if(e&&e instanceof cd)return e;const n=new cd;return n.parse(e,t),n}cd.prototype.parse=function(e,t){let n,r,i,o=e;if(o=o.trim(),!t&&1===e.split("#").length){const e=fd.exec(o);if(e)return this.pathname=e[1],e[2]&&(this.search=e[2]),this}let s=ud.exec(o);if(s&&(s=s[0],n=s.toLowerCase(),this.protocol=s,o=o.substr(s.length)),(t||s||o.match(/^\/\/[^@\/]+@[^@\/]+/))&&(i="//"===o.substr(0,2),!i||s&&bd[s]||(o=o.substr(2),this.slashes=!0)),!bd[s]&&(i||s&&!Ed[s])){let e,t,n=-1;for(let a=0;a127?r+="x":r+=n[e];if(!r.match(vd)){const r=e.slice(0,t),i=e.slice(t+1),s=n.match(yd);s&&(r.push(s[1]),i.unshift(s[2])),i.length&&(o=i.join(".")+o),this.hostname=r.join(".");break}}}}this.hostname.length>255&&(this.hostname=""),s&&(this.hostname=this.hostname.substr(1,this.hostname.length-2))}const a=o.indexOf("#");-1!==a&&(this.hash=o.substr(a),o=o.slice(0,a));const l=o.indexOf("?");return-1!==l&&(this.search=o.substr(l),o=o.slice(0,l)),o&&(this.pathname=o),Ed[n]&&this.hostname&&!this.pathname&&(this.pathname=""),this},cd.prototype.parseHost=function(e){let t=dd.exec(e);t&&(t=t[0],":"!==t&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)};const wd=Object.freeze(Object.defineProperty({__proto__:null,decode:od,encode:ad,format:ld,parse:xd},Symbol.toStringTag,{value:"Module"})),Td=/[\0-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,Cd=/[\0-\x1F\x7F-\x9F]/,Sd=/[!-#%-\*,-\/:;\?@\[-\]_\{\}\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061D-\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u09FD\u0A76\u0AF0\u0C77\u0C84\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1B7D\u1B7E\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E4F\u2E52-\u2E5D\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD803[\uDEAD\uDF55-\uDF59\uDF86-\uDF89]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC8\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDC4B-\uDC4F\uDC5A\uDC5B\uDC5D\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDE60-\uDE6C\uDEB9\uDF3C-\uDF3E]|\uD806[\uDC3B\uDD44-\uDD46\uDDE2\uDE3F-\uDE46\uDE9A-\uDE9C\uDE9E-\uDEA2\uDF00-\uDF09]|\uD807[\uDC41-\uDC45\uDC70\uDC71\uDEF7\uDEF8\uDF43-\uDF4F\uDFFF]|\uD809[\uDC70-\uDC74]|\uD80B[\uDFF1\uDFF2]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD81B[\uDE97-\uDE9A\uDFE2]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]|\uD83A[\uDD5E\uDD5F]/,kd=/[\$\+<->\^`\|~\xA2-\xA6\xA8\xA9\xAC\xAE-\xB1\xB4\xB8\xD7\xF7\u02C2-\u02C5\u02D2-\u02DF\u02E5-\u02EB\u02ED\u02EF-\u02FF\u0375\u0384\u0385\u03F6\u0482\u058D-\u058F\u0606-\u0608\u060B\u060E\u060F\u06DE\u06E9\u06FD\u06FE\u07F6\u07FE\u07FF\u0888\u09F2\u09F3\u09FA\u09FB\u0AF1\u0B70\u0BF3-\u0BFA\u0C7F\u0D4F\u0D79\u0E3F\u0F01-\u0F03\u0F13\u0F15-\u0F17\u0F1A-\u0F1F\u0F34\u0F36\u0F38\u0FBE-\u0FC5\u0FC7-\u0FCC\u0FCE\u0FCF\u0FD5-\u0FD8\u109E\u109F\u1390-\u1399\u166D\u17DB\u1940\u19DE-\u19FF\u1B61-\u1B6A\u1B74-\u1B7C\u1FBD\u1FBF-\u1FC1\u1FCD-\u1FCF\u1FDD-\u1FDF\u1FED-\u1FEF\u1FFD\u1FFE\u2044\u2052\u207A-\u207C\u208A-\u208C\u20A0-\u20C0\u2100\u2101\u2103-\u2106\u2108\u2109\u2114\u2116-\u2118\u211E-\u2123\u2125\u2127\u2129\u212E\u213A\u213B\u2140-\u2144\u214A-\u214D\u214F\u218A\u218B\u2190-\u2307\u230C-\u2328\u232B-\u2426\u2440-\u244A\u249C-\u24E9\u2500-\u2767\u2794-\u27C4\u27C7-\u27E5\u27F0-\u2982\u2999-\u29D7\u29DC-\u29FB\u29FE-\u2B73\u2B76-\u2B95\u2B97-\u2BFF\u2CE5-\u2CEA\u2E50\u2E51\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFF\u3004\u3012\u3013\u3020\u3036\u3037\u303E\u303F\u309B\u309C\u3190\u3191\u3196-\u319F\u31C0-\u31E3\u31EF\u3200-\u321E\u322A-\u3247\u3250\u3260-\u327F\u328A-\u32B0\u32C0-\u33FF\u4DC0-\u4DFF\uA490-\uA4C6\uA700-\uA716\uA720\uA721\uA789\uA78A\uA828-\uA82B\uA836-\uA839\uAA77-\uAA79\uAB5B\uAB6A\uAB6B\uFB29\uFBB2-\uFBC2\uFD40-\uFD4F\uFDCF\uFDFC-\uFDFF\uFE62\uFE64-\uFE66\uFE69\uFF04\uFF0B\uFF1C-\uFF1E\uFF3E\uFF40\uFF5C\uFF5E\uFFE0-\uFFE6\uFFE8-\uFFEE\uFFFC\uFFFD]|\uD800[\uDD37-\uDD3F\uDD79-\uDD89\uDD8C-\uDD8E\uDD90-\uDD9C\uDDA0\uDDD0-\uDDFC]|\uD802[\uDC77\uDC78\uDEC8]|\uD805\uDF3F|\uD807[\uDFD5-\uDFF1]|\uD81A[\uDF3C-\uDF3F\uDF45]|\uD82F\uDC9C|\uD833[\uDF50-\uDFC3]|\uD834[\uDC00-\uDCF5\uDD00-\uDD26\uDD29-\uDD64\uDD6A-\uDD6C\uDD83\uDD84\uDD8C-\uDDA9\uDDAE-\uDDEA\uDE00-\uDE41\uDE45\uDF00-\uDF56]|\uD835[\uDEC1\uDEDB\uDEFB\uDF15\uDF35\uDF4F\uDF6F\uDF89\uDFA9\uDFC3]|\uD836[\uDC00-\uDDFF\uDE37-\uDE3A\uDE6D-\uDE74\uDE76-\uDE83\uDE85\uDE86]|\uD838[\uDD4F\uDEFF]|\uD83B[\uDCAC\uDCB0\uDD2E\uDEF0\uDEF1]|\uD83C[\uDC00-\uDC2B\uDC30-\uDC93\uDCA0-\uDCAE\uDCB1-\uDCBF\uDCC1-\uDCCF\uDCD1-\uDCF5\uDD0D-\uDDAD\uDDE6-\uDE02\uDE10-\uDE3B\uDE40-\uDE48\uDE50\uDE51\uDE60-\uDE65\uDF00-\uDFFF]|\uD83D[\uDC00-\uDED7\uDEDC-\uDEEC\uDEF0-\uDEFC\uDF00-\uDF76\uDF7B-\uDFD9\uDFE0-\uDFEB\uDFF0]|\uD83E[\uDC00-\uDC0B\uDC10-\uDC47\uDC50-\uDC59\uDC60-\uDC87\uDC90-\uDCAD\uDCB0\uDCB1\uDD00-\uDE53\uDE60-\uDE6D\uDE70-\uDE7C\uDE80-\uDE88\uDE90-\uDEBD\uDEBF-\uDEC5\uDECE-\uDEDB\uDEE0-\uDEE8\uDEF0-\uDEF8\uDF00-\uDF92\uDF94-\uDFCA]/,_d=/[ \xA0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/,Nd=Object.freeze(Object.defineProperty({__proto__:null,Any:Td,Cc:Cd,Cf:/[\xAD\u0600-\u0605\u061C\u06DD\u070F\u0890\u0891\u08E2\u180E\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF\uFFF9-\uFFFB]|\uD804[\uDCBD\uDCCD]|\uD80D[\uDC30-\uDC3F]|\uD82F[\uDCA0-\uDCA3]|\uD834[\uDD73-\uDD7A]|\uDB40[\uDC01\uDC20-\uDC7F]/,P:Sd,S:kd,Z:_d},Symbol.toStringTag,{value:"Module"})),Dd=new Uint16Array('ᵁ<Õıʊҝջאٵ۞ޢߖࠏ੊ઑඡ๭༉༦჊ረዡᐕᒝᓃᓟᔥ\0\0\0\0\0\0ᕫᛍᦍᰒᷝ὾⁠↰⊍⏀⏻⑂⠤⤒ⴈ⹈⿎〖㊺㘹㞬㣾㨨㩱㫠㬮ࠀEMabcfglmnoprstu\\bfms„‹•˜¦³¹ÈÏlig耻Æ䃆P耻&䀦cute耻Á䃁reve;䄂Āiyx}rc耻Â䃂;䐐r;쀀𝔄rave耻À䃀pha;䎑acr;䄀d;橓Āgp¡on;䄄f;쀀𝔸plyFunction;恡ing耻Å䃅Ācs¾Ãr;쀀𝒜ign;扔ilde耻Ã䃃ml耻Ä䃄ЀaceforsuåûþėĜĢħĪĀcrêòkslash;或Ŷöø;櫧ed;挆y;䐑ƀcrtąċĔause;戵noullis;愬a;䎒r;쀀𝔅pf;쀀𝔹eve;䋘còēmpeq;扎܀HOacdefhilorsuōőŖƀƞƢƵƷƺǜȕɳɸɾcy;䐧PY耻©䂩ƀcpyŝŢźute;䄆Ā;iŧŨ拒talDifferentialD;慅leys;愭ȀaeioƉƎƔƘron;䄌dil耻Ç䃇rc;䄈nint;戰ot;䄊ĀdnƧƭilla;䂸terDot;䂷òſi;䎧rcleȀDMPTLJNjǑǖot;抙inus;抖lus;投imes;抗oĀcsǢǸkwiseContourIntegral;戲eCurlyĀDQȃȏoubleQuote;思uote;怙ȀlnpuȞȨɇɕonĀ;eȥȦ户;橴ƀgitȯȶȺruent;扡nt;戯ourIntegral;戮ĀfrɌɎ;愂oduct;成nterClockwiseContourIntegral;戳oss;樯cr;쀀𝒞pĀ;Cʄʅ拓ap;才րDJSZacefiosʠʬʰʴʸˋ˗ˡ˦̳ҍĀ;oŹʥtrahd;椑cy;䐂cy;䐅cy;䐏ƀgrsʿ˄ˇger;怡r;憡hv;櫤Āayː˕ron;䄎;䐔lĀ;t˝˞戇a;䎔r;쀀𝔇Āaf˫̧Ācm˰̢riticalȀADGT̖̜̀̆cute;䂴oŴ̋̍;䋙bleAcute;䋝rave;䁠ilde;䋜ond;拄ferentialD;慆Ѱ̽\0\0\0͔͂\0Ѕf;쀀𝔻ƀ;DE͈͉͍䂨ot;惜qual;扐blèCDLRUVͣͲ΂ϏϢϸontourIntegraìȹoɴ͹\0\0ͻ»͉nArrow;懓Āeo·ΤftƀARTΐΖΡrrow;懐ightArrow;懔eåˊngĀLRΫτeftĀARγιrrow;柸ightArrow;柺ightArrow;柹ightĀATϘϞrrow;懒ee;抨pɁϩ\0\0ϯrrow;懑ownArrow;懕erticalBar;戥ǹABLRTaВЪаўѿͼrrowƀ;BUНОТ憓ar;椓pArrow;懵reve;䌑eft˒к\0ц\0ѐightVector;楐eeVector;楞ectorĀ;Bљњ憽ar;楖ightǔѧ\0ѱeeVector;楟ectorĀ;BѺѻ懁ar;楗eeĀ;A҆҇护rrow;憧ĀctҒҗr;쀀𝒟rok;䄐ࠀNTacdfglmopqstuxҽӀӄӋӞӢӧӮӵԡԯԶՒ՝ՠեG;䅊H耻Ð䃐cute耻É䃉ƀaiyӒӗӜron;䄚rc耻Ê䃊;䐭ot;䄖r;쀀𝔈rave耻È䃈ement;戈ĀapӺӾcr;䄒tyɓԆ\0\0ԒmallSquare;旻erySmallSquare;斫ĀgpԦԪon;䄘f;쀀𝔼silon;䎕uĀaiԼՉlĀ;TՂՃ橵ilde;扂librium;懌Āci՗՚r;愰m;橳a;䎗ml耻Ë䃋Āipժկsts;戃onentialE;慇ʀcfiosօֈ֍ֲ׌y;䐤r;쀀𝔉lledɓ֗\0\0֣mallSquare;旼erySmallSquare;斪Ͱֺ\0ֿ\0\0ׄf;쀀𝔽All;戀riertrf;愱cò׋؀JTabcdfgorstר׬ׯ׺؀ؒؖ؛؝أ٬ٲcy;䐃耻>䀾mmaĀ;d׷׸䎓;䏜reve;䄞ƀeiy؇،ؐdil;䄢rc;䄜;䐓ot;䄠r;쀀𝔊;拙pf;쀀𝔾eater̀EFGLSTصلَٖٛ٦qualĀ;Lؾؿ扥ess;招ullEqual;执reater;檢ess;扷lantEqual;橾ilde;扳cr;쀀𝒢;扫ЀAacfiosuڅڋږڛڞڪھۊRDcy;䐪Āctڐڔek;䋇;䁞irc;䄤r;愌lbertSpace;愋ǰگ\0ڲf;愍izontalLine;攀Āctۃۅòکrok;䄦mpńېۘownHumðįqual;扏܀EJOacdfgmnostuۺ۾܃܇܎ܚܞܡܨ݄ݸދޏޕcy;䐕lig;䄲cy;䐁cute耻Í䃍Āiyܓܘrc耻Î䃎;䐘ot;䄰r;愑rave耻Ì䃌ƀ;apܠܯܿĀcgܴܷr;䄪inaryI;慈lieóϝǴ݉\0ݢĀ;eݍݎ戬Āgrݓݘral;戫section;拂isibleĀCTݬݲomma;恣imes;恢ƀgptݿރވon;䄮f;쀀𝕀a;䎙cr;愐ilde;䄨ǫޚ\0ޞcy;䐆l耻Ï䃏ʀcfosuެ޷޼߂ߐĀiyޱ޵rc;䄴;䐙r;쀀𝔍pf;쀀𝕁ǣ߇\0ߌr;쀀𝒥rcy;䐈kcy;䐄΀HJacfosߤߨ߽߬߱ࠂࠈcy;䐥cy;䐌ppa;䎚Āey߶߻dil;䄶;䐚r;쀀𝔎pf;쀀𝕂cr;쀀𝒦րJTaceflmostࠥࠩࠬࡐࡣ঳সে্਷ੇcy;䐉耻<䀼ʀcmnpr࠷࠼ࡁࡄࡍute;䄹bda;䎛g;柪lacetrf;愒r;憞ƀaeyࡗ࡜ࡡron;䄽dil;䄻;䐛Āfsࡨ॰tԀACDFRTUVarࡾࢩࢱࣦ࣠ࣼयज़ΐ४Ānrࢃ࢏gleBracket;柨rowƀ;BR࢙࢚࢞憐ar;懤ightArrow;懆eiling;挈oǵࢷ\0ࣃbleBracket;柦nǔࣈ\0࣒eeVector;楡ectorĀ;Bࣛࣜ懃ar;楙loor;挊ightĀAV࣯ࣵrrow;憔ector;楎Āerँगeƀ;AVउऊऐ抣rrow;憤ector;楚iangleƀ;BEतथऩ抲ar;槏qual;抴pƀDTVषूौownVector;楑eeVector;楠ectorĀ;Bॖॗ憿ar;楘ectorĀ;B॥०憼ar;楒ightáΜs̀EFGLSTॾঋকঝঢভqualGreater;拚ullEqual;扦reater;扶ess;檡lantEqual;橽ilde;扲r;쀀𝔏Ā;eঽা拘ftarrow;懚idot;䄿ƀnpw৔ਖਛgȀLRlr৞৷ਂਐeftĀAR০৬rrow;柵ightArrow;柷ightArrow;柶eftĀarγਊightáοightáϊf;쀀𝕃erĀLRਢਬeftArrow;憙ightArrow;憘ƀchtਾੀੂòࡌ;憰rok;䅁;扪Ѐacefiosuਗ਼੝੠੷੼અઋ઎p;椅y;䐜Ādl੥੯iumSpace;恟lintrf;愳r;쀀𝔐nusPlus;戓pf;쀀𝕄cò੶;䎜ҀJacefostuણધભીଔଙඑ඗ඞcy;䐊cute;䅃ƀaey઴હાron;䅇dil;䅅;䐝ƀgswે૰଎ativeƀMTV૓૟૨ediumSpace;怋hiĀcn૦૘ë૙eryThiî૙tedĀGL૸ଆreaterGreateòٳessLesóੈLine;䀊r;쀀𝔑ȀBnptଢନଷ଺reak;恠BreakingSpace;䂠f;愕ڀ;CDEGHLNPRSTV୕ୖ୪୼஡௫ఄ౞಄ದ೘ൡඅ櫬Āou୛୤ngruent;扢pCap;扭oubleVerticalBar;戦ƀlqxஃஊ஛ement;戉ualĀ;Tஒஓ扠ilde;쀀≂̸ists;戄reater΀;EFGLSTஶஷ஽௉௓௘௥扯qual;扱ullEqual;쀀≧̸reater;쀀≫̸ess;批lantEqual;쀀⩾̸ilde;扵umpń௲௽ownHump;쀀≎̸qual;쀀≏̸eĀfsఊధtTriangleƀ;BEచఛడ拪ar;쀀⧏̸qual;括s̀;EGLSTవశ఼ౄోౘ扮qual;扰reater;扸ess;쀀≪̸lantEqual;쀀⩽̸ilde;扴estedĀGL౨౹reaterGreater;쀀⪢̸essLess;쀀⪡̸recedesƀ;ESಒಓಛ技qual;쀀⪯̸lantEqual;拠ĀeiಫಹverseElement;戌ghtTriangleƀ;BEೋೌ೒拫ar;쀀⧐̸qual;拭ĀquೝഌuareSuĀbp೨೹setĀ;E೰ೳ쀀⊏̸qual;拢ersetĀ;Eഃആ쀀⊐̸qual;拣ƀbcpഓതൎsetĀ;Eഛഞ쀀⊂⃒qual;抈ceedsȀ;ESTലള഻െ抁qual;쀀⪰̸lantEqual;拡ilde;쀀≿̸ersetĀ;E൘൛쀀⊃⃒qual;抉ildeȀ;EFT൮൯൵ൿ扁qual;扄ullEqual;扇ilde;扉erticalBar;戤cr;쀀𝒩ilde耻Ñ䃑;䎝܀Eacdfgmoprstuvලෂ෉෕ෛ෠෧෼ขภยา฿ไlig;䅒cute耻Ó䃓Āiy෎ීrc耻Ô䃔;䐞blac;䅐r;쀀𝔒rave耻Ò䃒ƀaei෮ෲ෶cr;䅌ga;䎩cron;䎟pf;쀀𝕆enCurlyĀDQฎบoubleQuote;怜uote;怘;橔Āclวฬr;쀀𝒪ash耻Ø䃘iŬื฼de耻Õ䃕es;樷ml耻Ö䃖erĀBP๋๠Āar๐๓r;怾acĀek๚๜;揞et;掴arenthesis;揜Ҁacfhilors๿ງຊຏຒດຝະ໼rtialD;戂y;䐟r;쀀𝔓i;䎦;䎠usMinus;䂱Āipຢອncareplanåڝf;愙Ȁ;eio຺ູ໠໤檻cedesȀ;EST່້໏໚扺qual;檯lantEqual;扼ilde;找me;怳Ādp໩໮uct;戏ortionĀ;aȥ໹l;戝Āci༁༆r;쀀𝒫;䎨ȀUfos༑༖༛༟OT耻"䀢r;쀀𝔔pf;愚cr;쀀𝒬؀BEacefhiorsu༾གྷཇའཱིྦྷྪྭ႖ႩႴႾarr;椐G耻®䂮ƀcnrཎནབute;䅔g;柫rĀ;tཛྷཝ憠l;椖ƀaeyཧཬཱron;䅘dil;䅖;䐠Ā;vླྀཹ愜erseĀEUྂྙĀlq྇ྎement;戋uilibrium;懋pEquilibrium;楯r»ཹo;䎡ghtЀACDFTUVa࿁࿫࿳ဢဨၛႇϘĀnr࿆࿒gleBracket;柩rowƀ;BL࿜࿝࿡憒ar;懥eftArrow;懄eiling;按oǵ࿹\0စbleBracket;柧nǔည\0နeeVector;楝ectorĀ;Bဝသ懂ar;楕loor;挋Āerိ၃eƀ;AVဵံြ抢rrow;憦ector;楛iangleƀ;BEၐၑၕ抳ar;槐qual;抵pƀDTVၣၮၸownVector;楏eeVector;楜ectorĀ;Bႂႃ憾ar;楔ectorĀ;B႑႒懀ar;楓Āpuႛ႞f;愝ndImplies;楰ightarrow;懛ĀchႹႼr;愛;憱leDelayed;槴ڀHOacfhimoqstuფჱჷჽᄙᄞᅑᅖᅡᅧᆵᆻᆿĀCcჩხHcy;䐩y;䐨FTcy;䐬cute;䅚ʀ;aeiyᄈᄉᄎᄓᄗ檼ron;䅠dil;䅞rc;䅜;䐡r;쀀𝔖ortȀDLRUᄪᄴᄾᅉownArrow»ОeftArrow»࢚ightArrow»࿝pArrow;憑gma;䎣allCircle;战pf;쀀𝕊ɲᅭ\0\0ᅰt;戚areȀ;ISUᅻᅼᆉᆯ斡ntersection;抓uĀbpᆏᆞsetĀ;Eᆗᆘ抏qual;抑ersetĀ;Eᆨᆩ抐qual;抒nion;抔cr;쀀𝒮ar;拆ȀbcmpᇈᇛሉላĀ;sᇍᇎ拐etĀ;Eᇍᇕqual;抆ĀchᇠህeedsȀ;ESTᇭᇮᇴᇿ扻qual;檰lantEqual;扽ilde;承Tháྌ;我ƀ;esሒሓሣ拑rsetĀ;Eሜም抃qual;抇et»ሓրHRSacfhiorsሾቄ቉ቕ቞ቱቶኟዂወዑORN耻Þ䃞ADE;愢ĀHc቎ቒcy;䐋y;䐦Ābuቚቜ;䀉;䎤ƀaeyብቪቯron;䅤dil;䅢;䐢r;쀀𝔗Āeiቻ኉Dzኀ\0ኇefore;戴a;䎘Ācn኎ኘkSpace;쀀  Space;怉ldeȀ;EFTካኬኲኼ戼qual;扃ullEqual;扅ilde;扈pf;쀀𝕋ipleDot;惛Āctዖዛr;쀀𝒯rok;䅦ૡዷጎጚጦ\0ጬጱ\0\0\0\0\0ጸጽ፷ᎅ\0᏿ᐄᐊᐐĀcrዻጁute耻Ú䃚rĀ;oጇገ憟cir;楉rǣጓ\0጖y;䐎ve;䅬Āiyጞጣrc耻Û䃛;䐣blac;䅰r;쀀𝔘rave耻Ù䃙acr;䅪Ādiፁ፩erĀBPፈ፝Āarፍፐr;䁟acĀekፗፙ;揟et;掵arenthesis;揝onĀ;P፰፱拃lus;抎Āgp፻፿on;䅲f;쀀𝕌ЀADETadps᎕ᎮᎸᏄϨᏒᏗᏳrrowƀ;BDᅐᎠᎤar;椒ownArrow;懅ownArrow;憕quilibrium;楮eeĀ;AᏋᏌ报rrow;憥ownáϳerĀLRᏞᏨeftArrow;憖ightArrow;憗iĀ;lᏹᏺ䏒on;䎥ing;䅮cr;쀀𝒰ilde;䅨ml耻Ü䃜ҀDbcdefosvᐧᐬᐰᐳᐾᒅᒊᒐᒖash;披ar;櫫y;䐒ashĀ;lᐻᐼ抩;櫦Āerᑃᑅ;拁ƀbtyᑌᑐᑺar;怖Ā;iᑏᑕcalȀBLSTᑡᑥᑪᑴar;戣ine;䁼eparator;杘ilde;所ThinSpace;怊r;쀀𝔙pf;쀀𝕍cr;쀀𝒱dash;抪ʀcefosᒧᒬᒱᒶᒼirc;䅴dge;拀r;쀀𝔚pf;쀀𝕎cr;쀀𝒲Ȁfiosᓋᓐᓒᓘr;쀀𝔛;䎞pf;쀀𝕏cr;쀀𝒳ҀAIUacfosuᓱᓵᓹᓽᔄᔏᔔᔚᔠcy;䐯cy;䐇cy;䐮cute耻Ý䃝Āiyᔉᔍrc;䅶;䐫r;쀀𝔜pf;쀀𝕐cr;쀀𝒴ml;䅸ЀHacdefosᔵᔹᔿᕋᕏᕝᕠᕤcy;䐖cute;䅹Āayᕄᕉron;䅽;䐗ot;䅻Dzᕔ\0ᕛoWidtè૙a;䎖r;愨pf;愤cr;쀀𝒵௡ᖃᖊᖐ\0ᖰᖶᖿ\0\0\0\0ᗆᗛᗫᙟ᙭\0ᚕ᚛ᚲᚹ\0ᚾcute耻á䃡reve;䄃̀;Ediuyᖜᖝᖡᖣᖨᖭ戾;쀀∾̳;房rc耻â䃢te肻´̆;䐰lig耻æ䃦Ā;r²ᖺ;쀀𝔞rave耻à䃠ĀepᗊᗖĀfpᗏᗔsym;愵èᗓha;䎱ĀapᗟcĀclᗤᗧr;䄁g;樿ɤᗰ\0\0ᘊʀ;adsvᗺᗻᗿᘁᘇ戧nd;橕;橜lope;橘;橚΀;elmrszᘘᘙᘛᘞᘿᙏᙙ戠;榤e»ᘙsdĀ;aᘥᘦ戡ѡᘰᘲᘴᘶᘸᘺᘼᘾ;榨;榩;榪;榫;榬;榭;榮;榯tĀ;vᙅᙆ戟bĀ;dᙌᙍ抾;榝Āptᙔᙗh;戢»¹arr;捼Āgpᙣᙧon;䄅f;쀀𝕒΀;Eaeiop዁ᙻᙽᚂᚄᚇᚊ;橰cir;橯;扊d;手s;䀧roxĀ;e዁ᚒñᚃing耻å䃥ƀctyᚡᚦᚨr;쀀𝒶;䀪mpĀ;e዁ᚯñʈilde耻ã䃣ml耻ä䃤Āciᛂᛈoninôɲnt;樑ࠀNabcdefiklnoprsu᛭ᛱᜰ᜼ᝃᝈ᝸᝽០៦ᠹᡐᜍ᤽᥈ᥰot;櫭Ācrᛶ᜞kȀcepsᜀᜅᜍᜓong;扌psilon;䏶rime;怵imĀ;e᜚᜛戽q;拍Ŷᜢᜦee;抽edĀ;gᜬᜭ挅e»ᜭrkĀ;t፜᜷brk;掶Āoyᜁᝁ;䐱quo;怞ʀcmprtᝓ᝛ᝡᝤᝨausĀ;eĊĉptyv;榰séᜌnoõēƀahwᝯ᝱ᝳ;䎲;愶een;扬r;쀀𝔟g΀costuvwឍឝឳេ៕៛៞ƀaiuបពរðݠrc;旯p»፱ƀdptឤឨឭot;樀lus;樁imes;樂ɱឹ\0\0ើcup;樆ar;昅riangleĀdu៍្own;施p;斳plus;樄eåᑄåᒭarow;植ƀako៭ᠦᠵĀcn៲ᠣkƀlst៺֫᠂ozenge;槫riangleȀ;dlr᠒᠓᠘᠝斴own;斾eft;旂ight;斸k;搣Ʊᠫ\0ᠳƲᠯ\0ᠱ;斒;斑4;斓ck;斈ĀeoᠾᡍĀ;qᡃᡆ쀀=⃥uiv;쀀≡⃥t;挐Ȁptwxᡙᡞᡧᡬf;쀀𝕓Ā;tᏋᡣom»Ꮜtie;拈؀DHUVbdhmptuvᢅᢖᢪᢻᣗᣛᣬ᣿ᤅᤊᤐᤡȀLRlrᢎᢐᢒᢔ;敗;敔;敖;敓ʀ;DUduᢡᢢᢤᢦᢨ敐;敦;敩;敤;敧ȀLRlrᢳᢵᢷᢹ;敝;敚;敜;教΀;HLRhlrᣊᣋᣍᣏᣑᣓᣕ救;敬;散;敠;敫;敢;敟ox;槉ȀLRlrᣤᣦᣨᣪ;敕;敒;攐;攌ʀ;DUduڽ᣷᣹᣻᣽;敥;敨;攬;攴inus;抟lus;択imes;抠ȀLRlrᤙᤛᤝ᤟;敛;敘;攘;攔΀;HLRhlrᤰᤱᤳᤵᤷ᤻᤹攂;敪;敡;敞;攼;攤;攜Āevģ᥂bar耻¦䂦Ȁceioᥑᥖᥚᥠr;쀀𝒷mi;恏mĀ;e᜚᜜lƀ;bhᥨᥩᥫ䁜;槅sub;柈Ŭᥴ᥾lĀ;e᥹᥺怢t»᥺pƀ;Eeįᦅᦇ;檮Ā;qۜۛೡᦧ\0᧨ᨑᨕᨲ\0ᨷᩐ\0\0᪴\0\0᫁\0\0ᬡᬮ᭍᭒\0᯽\0ᰌƀcpr᦭ᦲ᧝ute;䄇̀;abcdsᦿᧀᧄ᧊᧕᧙戩nd;橄rcup;橉Āau᧏᧒p;橋p;橇ot;橀;쀀∩︀Āeo᧢᧥t;恁îړȀaeiu᧰᧻ᨁᨅǰ᧵\0᧸s;橍on;䄍dil耻ç䃧rc;䄉psĀ;sᨌᨍ橌m;橐ot;䄋ƀdmnᨛᨠᨦil肻¸ƭptyv;榲t脀¢;eᨭᨮ䂢räƲr;쀀𝔠ƀceiᨽᩀᩍy;䑇ckĀ;mᩇᩈ朓ark»ᩈ;䏇r΀;Ecefms᩟᩠ᩢᩫ᪤᪪᪮旋;槃ƀ;elᩩᩪᩭ䋆q;扗eɡᩴ\0\0᪈rrowĀlr᩼᪁eft;憺ight;憻ʀRSacd᪒᪔᪖᪚᪟»ཇ;擈st;抛irc;抚ash;抝nint;樐id;櫯cir;槂ubsĀ;u᪻᪼晣it»᪼ˬ᫇᫔᫺\0ᬊonĀ;eᫍᫎ䀺Ā;qÇÆɭ᫙\0\0᫢aĀ;t᫞᫟䀬;䁀ƀ;fl᫨᫩᫫戁îᅠeĀmx᫱᫶ent»᫩eóɍǧ᫾\0ᬇĀ;dኻᬂot;橭nôɆƀfryᬐᬔᬗ;쀀𝕔oäɔ脀©;sŕᬝr;愗Āaoᬥᬩrr;憵ss;朗Ācuᬲᬷr;쀀𝒸Ābpᬼ᭄Ā;eᭁᭂ櫏;櫑Ā;eᭉᭊ櫐;櫒dot;拯΀delprvw᭠᭬᭷ᮂᮬᯔ᯹arrĀlr᭨᭪;椸;椵ɰ᭲\0\0᭵r;拞c;拟arrĀ;p᭿ᮀ憶;椽̀;bcdosᮏᮐᮖᮡᮥᮨ截rcap;橈Āauᮛᮞp;橆p;橊ot;抍r;橅;쀀∪︀Ȁalrv᮵ᮿᯞᯣrrĀ;mᮼᮽ憷;椼yƀevwᯇᯔᯘqɰᯎ\0\0ᯒreã᭳uã᭵ee;拎edge;拏en耻¤䂤earrowĀlrᯮ᯳eft»ᮀight»ᮽeäᯝĀciᰁᰇoninôǷnt;戱lcty;挭ঀAHabcdefhijlorstuwz᰸᰻᰿ᱝᱩᱵᲊᲞᲬᲷ᳻᳿ᴍᵻᶑᶫᶻ᷆᷍rò΁ar;楥Ȁglrs᱈ᱍ᱒᱔ger;怠eth;愸òᄳhĀ;vᱚᱛ怐»ऊūᱡᱧarow;椏aã̕Āayᱮᱳron;䄏;䐴ƀ;ao̲ᱼᲄĀgrʿᲁr;懊tseq;橷ƀglmᲑᲔᲘ耻°䂰ta;䎴ptyv;榱ĀirᲣᲨsht;楿;쀀𝔡arĀlrᲳᲵ»ࣜ»သʀaegsv᳂͸᳖᳜᳠mƀ;oș᳊᳔ndĀ;ș᳑uit;晦amma;䏝in;拲ƀ;io᳧᳨᳸䃷de脀÷;o᳧ᳰntimes;拇nø᳷cy;䑒cɯᴆ\0\0ᴊrn;挞op;挍ʀlptuwᴘᴝᴢᵉᵕlar;䀤f;쀀𝕕ʀ;emps̋ᴭᴷᴽᵂqĀ;d͒ᴳot;扑inus;戸lus;戔quare;抡blebarwedgåúnƀadhᄮᵝᵧownarrowóᲃarpoonĀlrᵲᵶefôᲴighôᲶŢᵿᶅkaro÷གɯᶊ\0\0ᶎrn;挟op;挌ƀcotᶘᶣᶦĀryᶝᶡ;쀀𝒹;䑕l;槶rok;䄑Ādrᶰᶴot;拱iĀ;fᶺ᠖斿Āah᷀᷃ròЩaòྦangle;榦Āci᷒ᷕy;䑟grarr;柿ऀDacdefglmnopqrstuxḁḉḙḸոḼṉṡṾấắẽỡἪἷὄ὎὚ĀDoḆᴴoôᲉĀcsḎḔute耻é䃩ter;橮ȀaioyḢḧḱḶron;䄛rĀ;cḭḮ扖耻ê䃪lon;払;䑍ot;䄗ĀDrṁṅot;扒;쀀𝔢ƀ;rsṐṑṗ檚ave耻è䃨Ā;dṜṝ檖ot;檘Ȁ;ilsṪṫṲṴ檙nters;揧;愓Ā;dṹṺ檕ot;檗ƀapsẅẉẗcr;䄓tyƀ;svẒẓẕ戅et»ẓpĀ1;ẝẤijạả;怄;怅怃ĀgsẪẬ;䅋p;怂ĀgpẴẸon;䄙f;쀀𝕖ƀalsỄỎỒrĀ;sỊị拕l;槣us;橱iƀ;lvỚớở䎵on»ớ;䏵ȀcsuvỪỳἋἣĀioữḱrc»Ḯɩỹ\0\0ỻíՈantĀglἂἆtr»ṝess»Ṻƀaeiἒ἖Ἒls;䀽st;扟vĀ;DȵἠD;橸parsl;槥ĀDaἯἳot;打rr;楱ƀcdiἾὁỸr;愯oô͒ĀahὉὋ;䎷耻ð䃰Āmrὓὗl耻ë䃫o;悬ƀcipὡὤὧl;䀡sôծĀeoὬὴctatioîՙnentialåչৡᾒ\0ᾞ\0ᾡᾧ\0\0ῆῌ\0ΐ\0ῦῪ \0 ⁚llingdotseñṄy;䑄male;晀ƀilrᾭᾳ῁lig;耀ffiɩᾹ\0\0᾽g;耀ffig;耀ffl;쀀𝔣lig;耀filig;쀀fjƀaltῙ῜ῡt;晭ig;耀flns;斱of;䆒ǰ΅\0ῳf;쀀𝕗ĀakֿῷĀ;vῼ´拔;櫙artint;樍Āao‌⁕Ācs‑⁒ႉ‸⁅⁈\0⁐β•‥‧‪‬\0‮耻½䂽;慓耻¼䂼;慕;慙;慛Ƴ‴\0‶;慔;慖ʴ‾⁁\0\0⁃耻¾䂾;慗;慜5;慘ƶ⁌\0⁎;慚;慝8;慞l;恄wn;挢cr;쀀𝒻ࢀEabcdefgijlnorstv₂₉₟₥₰₴⃰⃵⃺⃿℃ℒℸ̗ℾ⅒↞Ā;lٍ₇;檌ƀcmpₐₕ₝ute;䇵maĀ;dₜ᳚䎳;檆reve;䄟Āiy₪₮rc;䄝;䐳ot;䄡Ȁ;lqsؾق₽⃉ƀ;qsؾٌ⃄lanô٥Ȁ;cdl٥⃒⃥⃕c;檩otĀ;o⃜⃝檀Ā;l⃢⃣檂;檄Ā;e⃪⃭쀀⋛︀s;檔r;쀀𝔤Ā;gٳ؛mel;愷cy;䑓Ȁ;Eajٚℌℎℐ;檒;檥;檤ȀEaesℛℝ℩ℴ;扩pĀ;p℣ℤ檊rox»ℤĀ;q℮ℯ檈Ā;q℮ℛim;拧pf;쀀𝕘Āci⅃ⅆr;愊mƀ;el٫ⅎ⅐;檎;檐茀>;cdlqr׮ⅠⅪⅮⅳⅹĀciⅥⅧ;檧r;橺ot;拗Par;榕uest;橼ʀadelsↄⅪ←ٖ↛ǰ↉\0↎proø₞r;楸qĀlqؿ↖lesó₈ií٫Āen↣↭rtneqq;쀀≩︀Å↪ԀAabcefkosy⇄⇇⇱⇵⇺∘∝∯≨≽ròΠȀilmr⇐⇔⇗⇛rsðᒄf»․ilôکĀdr⇠⇤cy;䑊ƀ;cwࣴ⇫⇯ir;楈;憭ar;意irc;䄥ƀalr∁∎∓rtsĀ;u∉∊晥it»∊lip;怦con;抹r;쀀𝔥sĀew∣∩arow;椥arow;椦ʀamopr∺∾≃≞≣rr;懿tht;戻kĀlr≉≓eftarrow;憩ightarrow;憪f;쀀𝕙bar;怕ƀclt≯≴≸r;쀀𝒽asè⇴rok;䄧Ābp⊂⊇ull;恃hen»ᱛૡ⊣\0⊪\0⊸⋅⋎\0⋕⋳\0\0⋸⌢⍧⍢⍿\0⎆⎪⎴cute耻í䃭ƀ;iyݱ⊰⊵rc耻î䃮;䐸Ācx⊼⊿y;䐵cl耻¡䂡ĀfrΟ⋉;쀀𝔦rave耻ì䃬Ȁ;inoܾ⋝⋩⋮Āin⋢⋦nt;樌t;戭fin;槜ta;愩lig;䄳ƀaop⋾⌚⌝ƀcgt⌅⌈⌗r;䄫ƀelpܟ⌏⌓inåގarôܠh;䄱f;抷ed;䆵ʀ;cfotӴ⌬⌱⌽⍁are;愅inĀ;t⌸⌹戞ie;槝doô⌙ʀ;celpݗ⍌⍐⍛⍡al;抺Āgr⍕⍙eróᕣã⍍arhk;樗rod;樼Ȁcgpt⍯⍲⍶⍻y;䑑on;䄯f;쀀𝕚a;䎹uest耻¿䂿Āci⎊⎏r;쀀𝒾nʀ;EdsvӴ⎛⎝⎡ӳ;拹ot;拵Ā;v⎦⎧拴;拳Ā;iݷ⎮lde;䄩ǫ⎸\0⎼cy;䑖l耻ï䃯̀cfmosu⏌⏗⏜⏡⏧⏵Āiy⏑⏕rc;䄵;䐹r;쀀𝔧ath;䈷pf;쀀𝕛ǣ⏬\0⏱r;쀀𝒿rcy;䑘kcy;䑔Ѐacfghjos␋␖␢␧␭␱␵␻ppaĀ;v␓␔䎺;䏰Āey␛␠dil;䄷;䐺r;쀀𝔨reen;䄸cy;䑅cy;䑜pf;쀀𝕜cr;쀀𝓀஀ABEHabcdefghjlmnoprstuv⑰⒁⒆⒍⒑┎┽╚▀♎♞♥♹♽⚚⚲⛘❝❨➋⟀⠁⠒ƀart⑷⑺⑼rò৆òΕail;椛arr;椎Ā;gঔ⒋;檋ar;楢ॣ⒥\0⒪\0⒱\0\0\0\0\0⒵Ⓔ\0ⓆⓈⓍ\0⓹ute;䄺mptyv;榴raîࡌbda;䎻gƀ;dlࢎⓁⓃ;榑åࢎ;檅uo耻«䂫rЀ;bfhlpst࢙ⓞⓦⓩ⓫⓮⓱⓵Ā;f࢝ⓣs;椟s;椝ë≒p;憫l;椹im;楳l;憢ƀ;ae⓿─┄檫il;椙Ā;s┉┊檭;쀀⪭︀ƀabr┕┙┝rr;椌rk;杲Āak┢┬cĀek┨┪;䁻;䁛Āes┱┳;榋lĀdu┹┻;榏;榍Ȁaeuy╆╋╖╘ron;䄾Ādi═╔il;䄼ìࢰâ┩;䐻Ȁcqrs╣╦╭╽a;椶uoĀ;rนᝆĀdu╲╷har;楧shar;楋h;憲ʀ;fgqs▋▌উ◳◿扤tʀahlrt▘▤▷◂◨rrowĀ;t࢙□aé⓶arpoonĀdu▯▴own»њp»०eftarrows;懇ightƀahs◍◖◞rrowĀ;sࣴࢧarpoonó྘quigarro÷⇰hreetimes;拋ƀ;qs▋ও◺lanôবʀ;cdgsব☊☍☝☨c;檨otĀ;o☔☕橿Ā;r☚☛檁;檃Ā;e☢☥쀀⋚︀s;檓ʀadegs☳☹☽♉♋pproøⓆot;拖qĀgq♃♅ôউgtò⒌ôছiíলƀilr♕࣡♚sht;楼;쀀𝔩Ā;Eজ♣;檑š♩♶rĀdu▲♮Ā;l॥♳;楪lk;斄cy;䑙ʀ;achtੈ⚈⚋⚑⚖rò◁orneòᴈard;楫ri;旺Āio⚟⚤dot;䅀ustĀ;a⚬⚭掰che»⚭ȀEaes⚻⚽⛉⛔;扨pĀ;p⛃⛄檉rox»⛄Ā;q⛎⛏檇Ā;q⛎⚻im;拦Ѐabnoptwz⛩⛴⛷✚✯❁❇❐Ānr⛮⛱g;柬r;懽rëࣁgƀlmr⛿✍✔eftĀar০✇ightá৲apsto;柼ightá৽parrowĀlr✥✩efô⓭ight;憬ƀafl✶✹✽r;榅;쀀𝕝us;樭imes;樴š❋❏st;戗áፎƀ;ef❗❘᠀旊nge»❘arĀ;l❤❥䀨t;榓ʀachmt❳❶❼➅➇ròࢨorneòᶌarĀ;d྘➃;業;怎ri;抿̀achiqt➘➝ੀ➢➮➻quo;怹r;쀀𝓁mƀ;egল➪➬;檍;檏Ābu┪➳oĀ;rฟ➹;怚rok;䅂萀<;cdhilqrࠫ⟒☹⟜⟠⟥⟪⟰Āci⟗⟙;檦r;橹reå◲mes;拉arr;楶uest;橻ĀPi⟵⟹ar;榖ƀ;ef⠀भ᠛旃rĀdu⠇⠍shar;楊har;楦Āen⠗⠡rtneqq;쀀≨︀Å⠞܀Dacdefhilnopsu⡀⡅⢂⢎⢓⢠⢥⢨⣚⣢⣤ઃ⣳⤂Dot;戺Ȁclpr⡎⡒⡣⡽r耻¯䂯Āet⡗⡙;時Ā;e⡞⡟朠se»⡟Ā;sျ⡨toȀ;dluျ⡳⡷⡻owîҌefôएðᏑker;斮Āoy⢇⢌mma;権;䐼ash;怔asuredangle»ᘦr;쀀𝔪o;愧ƀcdn⢯⢴⣉ro耻µ䂵Ȁ;acdᑤ⢽⣀⣄sôᚧir;櫰ot肻·Ƶusƀ;bd⣒ᤃ⣓戒Ā;uᴼ⣘;横ţ⣞⣡p;櫛ò−ðઁĀdp⣩⣮els;抧f;쀀𝕞Āct⣸⣽r;쀀𝓂pos»ᖝƀ;lm⤉⤊⤍䎼timap;抸ఀGLRVabcdefghijlmoprstuvw⥂⥓⥾⦉⦘⧚⧩⨕⨚⩘⩝⪃⪕⪤⪨⬄⬇⭄⭿⮮ⰴⱧⱼ⳩Āgt⥇⥋;쀀⋙̸Ā;v⥐௏쀀≫⃒ƀelt⥚⥲⥶ftĀar⥡⥧rrow;懍ightarrow;懎;쀀⋘̸Ā;v⥻ే쀀≪⃒ightarrow;懏ĀDd⦎⦓ash;抯ash;抮ʀbcnpt⦣⦧⦬⦱⧌la»˞ute;䅄g;쀀∠⃒ʀ;Eiop඄⦼⧀⧅⧈;쀀⩰̸d;쀀≋̸s;䅉roø඄urĀ;a⧓⧔普lĀ;s⧓ସdz⧟\0⧣p肻 ଷmpĀ;e௹ఀʀaeouy⧴⧾⨃⨐⨓ǰ⧹\0⧻;橃on;䅈dil;䅆ngĀ;dൾ⨊ot;쀀⩭̸p;橂;䐽ash;怓΀;Aadqsxஒ⨩⨭⨻⩁⩅⩐rr;懗rĀhr⨳⨶k;椤Ā;oᏲᏰot;쀀≐̸uiöୣĀei⩊⩎ar;椨í஘istĀ;s஠டr;쀀𝔫ȀEest௅⩦⩹⩼ƀ;qs஼⩭௡ƀ;qs஼௅⩴lanô௢ií௪Ā;rஶ⪁»ஷƀAap⪊⪍⪑rò⥱rr;憮ar;櫲ƀ;svྍ⪜ྌĀ;d⪡⪢拼;拺cy;䑚΀AEadest⪷⪺⪾⫂⫅⫶⫹rò⥦;쀀≦̸rr;憚r;急Ȁ;fqs఻⫎⫣⫯tĀar⫔⫙rro÷⫁ightarro÷⪐ƀ;qs఻⪺⫪lanôౕĀ;sౕ⫴»శiíౝĀ;rవ⫾iĀ;eచథiäඐĀpt⬌⬑f;쀀𝕟膀¬;in⬙⬚⬶䂬nȀ;Edvஉ⬤⬨⬮;쀀⋹̸ot;쀀⋵̸ǡஉ⬳⬵;拷;拶iĀ;vಸ⬼ǡಸ⭁⭃;拾;拽ƀaor⭋⭣⭩rȀ;ast୻⭕⭚⭟lleì୻l;쀀⫽⃥;쀀∂̸lint;樔ƀ;ceಒ⭰⭳uåಥĀ;cಘ⭸Ā;eಒ⭽ñಘȀAait⮈⮋⮝⮧rò⦈rrƀ;cw⮔⮕⮙憛;쀀⤳̸;쀀↝̸ghtarrow»⮕riĀ;eೋೖ΀chimpqu⮽⯍⯙⬄୸⯤⯯Ȁ;cerല⯆ഷ⯉uå൅;쀀𝓃ortɭ⬅\0\0⯖ará⭖mĀ;e൮⯟Ā;q൴൳suĀbp⯫⯭å೸åഋƀbcp⯶ⰑⰙȀ;Ees⯿ⰀഢⰄ抄;쀀⫅̸etĀ;eഛⰋqĀ;qണⰀcĀ;eലⰗñസȀ;EesⰢⰣൟⰧ抅;쀀⫆̸etĀ;e൘ⰮqĀ;qൠⰣȀgilrⰽⰿⱅⱇìௗlde耻ñ䃱çృiangleĀlrⱒⱜeftĀ;eచⱚñదightĀ;eೋⱥñ೗Ā;mⱬⱭ䎽ƀ;esⱴⱵⱹ䀣ro;愖p;怇ҀDHadgilrsⲏⲔⲙⲞⲣⲰⲶⳓⳣash;抭arr;椄p;쀀≍⃒ash;抬ĀetⲨⲬ;쀀≥⃒;쀀>⃒nfin;槞ƀAetⲽⳁⳅrr;椂;쀀≤⃒Ā;rⳊⳍ쀀<⃒ie;쀀⊴⃒ĀAtⳘⳜrr;椃rie;쀀⊵⃒im;쀀∼⃒ƀAan⳰⳴ⴂrr;懖rĀhr⳺⳽k;椣Ā;oᏧᏥear;椧ቓ᪕\0\0\0\0\0\0\0\0\0\0\0\0\0ⴭ\0ⴸⵈⵠⵥ⵲ⶄᬇ\0\0ⶍⶫ\0ⷈⷎ\0ⷜ⸙⸫⸾⹃Ācsⴱ᪗ute耻ó䃳ĀiyⴼⵅrĀ;c᪞ⵂ耻ô䃴;䐾ʀabios᪠ⵒⵗLjⵚlac;䅑v;樸old;榼lig;䅓Ācr⵩⵭ir;榿;쀀𝔬ͯ⵹\0\0⵼\0ⶂn;䋛ave耻ò䃲;槁Ābmⶈ෴ar;榵Ȁacitⶕ⶘ⶥⶨrò᪀Āir⶝ⶠr;榾oss;榻nå๒;槀ƀaeiⶱⶵⶹcr;䅍ga;䏉ƀcdnⷀⷅǍron;䎿;榶pf;쀀𝕠ƀaelⷔ⷗ǒr;榷rp;榹΀;adiosvⷪⷫⷮ⸈⸍⸐⸖戨rò᪆Ȁ;efmⷷⷸ⸂⸅橝rĀ;oⷾⷿ愴f»ⷿ耻ª䂪耻º䂺gof;抶r;橖lope;橗;橛ƀclo⸟⸡⸧ò⸁ash耻ø䃸l;折iŬⸯ⸴de耻õ䃵esĀ;aǛ⸺s;樶ml耻ö䃶bar;挽ૡ⹞\0⹽\0⺀⺝\0⺢⺹\0\0⻋ຜ\0⼓\0\0⼫⾼\0⿈rȀ;astЃ⹧⹲຅脀¶;l⹭⹮䂶leìЃɩ⹸\0\0⹻m;櫳;櫽y;䐿rʀcimpt⺋⺏⺓ᡥ⺗nt;䀥od;䀮il;怰enk;怱r;쀀𝔭ƀimo⺨⺰⺴Ā;v⺭⺮䏆;䏕maô੶ne;明ƀ;tv⺿⻀⻈䏀chfork»´;䏖Āau⻏⻟nĀck⻕⻝kĀ;h⇴⻛;愎ö⇴sҀ;abcdemst⻳⻴ᤈ⻹⻽⼄⼆⼊⼎䀫cir;樣ir;樢Āouᵀ⼂;樥;橲n肻±ຝim;樦wo;樧ƀipu⼙⼠⼥ntint;樕f;쀀𝕡nd耻£䂣Ԁ;Eaceinosu່⼿⽁⽄⽇⾁⾉⾒⽾⾶;檳p;檷uå໙Ā;c໎⽌̀;acens່⽙⽟⽦⽨⽾pproø⽃urlyeñ໙ñ໎ƀaes⽯⽶⽺pprox;檹qq;檵im;拨iíໟmeĀ;s⾈ຮ怲ƀEas⽸⾐⽺ð⽵ƀdfp໬⾙⾯ƀals⾠⾥⾪lar;挮ine;挒urf;挓Ā;t໻⾴ï໻rel;抰Āci⿀⿅r;쀀𝓅;䏈ncsp;怈̀fiopsu⿚⋢⿟⿥⿫⿱r;쀀𝔮pf;쀀𝕢rime;恗cr;쀀𝓆ƀaeo⿸〉〓tĀei⿾々rnionóڰnt;樖stĀ;e【】䀿ñἙô༔઀ABHabcdefhilmnoprstux぀けさすムㄎㄫㅇㅢㅲㆎ㈆㈕㈤㈩㉘㉮㉲㊐㊰㊷ƀartぇおがròႳòϝail;検aròᱥar;楤΀cdenqrtとふへみわゔヌĀeuねぱ;쀀∽̱te;䅕iãᅮmptyv;榳gȀ;del࿑らるろ;榒;榥å࿑uo耻»䂻rր;abcfhlpstw࿜ガクシスゼゾダッデナp;極Ā;f࿠ゴs;椠;椳s;椞ë≝ð✮l;楅im;楴l;憣;憝Āaiパフil;椚oĀ;nホボ戶aló༞ƀabrョリヮrò៥rk;杳ĀakンヽcĀekヹ・;䁽;䁝Āes㄂㄄;榌lĀduㄊㄌ;榎;榐Ȁaeuyㄗㄜㄧㄩron;䅙Ādiㄡㄥil;䅗ì࿲âヺ;䑀Ȁclqsㄴㄷㄽㅄa;椷dhar;楩uoĀ;rȎȍh;憳ƀacgㅎㅟངlȀ;ipsླྀㅘㅛႜnåႻarôྩt;断ƀilrㅩဣㅮsht;楽;쀀𝔯ĀaoㅷㆆrĀduㅽㅿ»ѻĀ;l႑ㆄ;楬Ā;vㆋㆌ䏁;䏱ƀgns㆕ㇹㇼht̀ahlrstㆤㆰ㇂㇘㇤㇮rrowĀ;t࿜ㆭaéトarpoonĀduㆻㆿowîㅾp»႒eftĀah㇊㇐rrowó࿪arpoonóՑightarrows;應quigarro÷ニhreetimes;拌g;䋚ingdotseñἲƀahm㈍㈐㈓rò࿪aòՑ;怏oustĀ;a㈞㈟掱che»㈟mid;櫮Ȁabpt㈲㈽㉀㉒Ānr㈷㈺g;柭r;懾rëဃƀafl㉇㉊㉎r;榆;쀀𝕣us;樮imes;樵Āap㉝㉧rĀ;g㉣㉤䀩t;榔olint;樒arò㇣Ȁachq㉻㊀Ⴜ㊅quo;怺r;쀀𝓇Ābu・㊊oĀ;rȔȓƀhir㊗㊛㊠reåㇸmes;拊iȀ;efl㊪ၙᠡ㊫方tri;槎luhar;楨;愞ൡ㋕㋛㋟㌬㌸㍱\0㍺㎤\0\0㏬㏰\0㐨㑈㑚㒭㒱㓊㓱\0㘖\0\0㘳cute;䅛quï➺Ԁ;Eaceinpsyᇭ㋳㋵㋿㌂㌋㌏㌟㌦㌩;檴ǰ㋺\0㋼;檸on;䅡uåᇾĀ;dᇳ㌇il;䅟rc;䅝ƀEas㌖㌘㌛;檶p;檺im;择olint;樓iíሄ;䑁otƀ;be㌴ᵇ㌵担;橦΀Aacmstx㍆㍊㍗㍛㍞㍣㍭rr;懘rĀhr㍐㍒ë∨Ā;oਸ਼਴t耻§䂧i;䀻war;椩mĀin㍩ðnuóñt;朶rĀ;o㍶⁕쀀𝔰Ȁacoy㎂㎆㎑㎠rp;景Āhy㎋㎏cy;䑉;䑈rtɭ㎙\0\0㎜iäᑤaraì⹯耻­䂭Āgm㎨㎴maƀ;fv㎱㎲㎲䏃;䏂Ѐ;deglnprካ㏅㏉㏎㏖㏞㏡㏦ot;橪Ā;q኱ኰĀ;E㏓㏔檞;檠Ā;E㏛㏜檝;檟e;扆lus;樤arr;楲aròᄽȀaeit㏸㐈㐏㐗Āls㏽㐄lsetmé㍪hp;樳parsl;槤Ādlᑣ㐔e;挣Ā;e㐜㐝檪Ā;s㐢㐣檬;쀀⪬︀ƀflp㐮㐳㑂tcy;䑌Ā;b㐸㐹䀯Ā;a㐾㐿槄r;挿f;쀀𝕤aĀdr㑍ЂesĀ;u㑔㑕晠it»㑕ƀcsu㑠㑹㒟Āau㑥㑯pĀ;sᆈ㑫;쀀⊓︀pĀ;sᆴ㑵;쀀⊔︀uĀbp㑿㒏ƀ;esᆗᆜ㒆etĀ;eᆗ㒍ñᆝƀ;esᆨᆭ㒖etĀ;eᆨ㒝ñᆮƀ;afᅻ㒦ְrť㒫ֱ»ᅼaròᅈȀcemt㒹㒾㓂㓅r;쀀𝓈tmîñiì㐕aræᆾĀar㓎㓕rĀ;f㓔ឿ昆Āan㓚㓭ightĀep㓣㓪psiloîỠhé⺯s»⡒ʀbcmnp㓻㕞ሉ㖋㖎Ҁ;Edemnprs㔎㔏㔑㔕㔞㔣㔬㔱㔶抂;櫅ot;檽Ā;dᇚ㔚ot;櫃ult;櫁ĀEe㔨㔪;櫋;把lus;檿arr;楹ƀeiu㔽㕒㕕tƀ;en㔎㕅㕋qĀ;qᇚ㔏eqĀ;q㔫㔨m;櫇Ābp㕚㕜;櫕;櫓c̀;acensᇭ㕬㕲㕹㕻㌦pproø㋺urlyeñᇾñᇳƀaes㖂㖈㌛pproø㌚qñ㌗g;晪ڀ123;Edehlmnps㖩㖬㖯ሜ㖲㖴㗀㗉㗕㗚㗟㗨㗭耻¹䂹耻²䂲耻³䂳;櫆Āos㖹㖼t;檾ub;櫘Ā;dሢ㗅ot;櫄sĀou㗏㗒l;柉b;櫗arr;楻ult;櫂ĀEe㗤㗦;櫌;抋lus;櫀ƀeiu㗴㘉㘌tƀ;enሜ㗼㘂qĀ;qሢ㖲eqĀ;q㗧㗤m;櫈Ābp㘑㘓;櫔;櫖ƀAan㘜㘠㘭rr;懙rĀhr㘦㘨ë∮Ā;oਫ਩war;椪lig耻ß䃟௡㙑㙝㙠ዎ㙳㙹\0㙾㛂\0\0\0\0\0㛛㜃\0㜉㝬\0\0\0㞇ɲ㙖\0\0㙛get;挖;䏄rë๟ƀaey㙦㙫㙰ron;䅥dil;䅣;䑂lrec;挕r;쀀𝔱Ȁeiko㚆㚝㚵㚼Dz㚋\0㚑eĀ4fኄኁaƀ;sv㚘㚙㚛䎸ym;䏑Ācn㚢㚲kĀas㚨㚮pproø዁im»ኬsðኞĀas㚺㚮ð዁rn耻þ䃾Ǭ̟㛆⋧es膀×;bd㛏㛐㛘䃗Ā;aᤏ㛕r;樱;樰ƀeps㛡㛣㜀á⩍Ȁ;bcf҆㛬㛰㛴ot;挶ir;櫱Ā;o㛹㛼쀀𝕥rk;櫚á㍢rime;怴ƀaip㜏㜒㝤dåቈ΀adempst㜡㝍㝀㝑㝗㝜㝟ngleʀ;dlqr㜰㜱㜶㝀㝂斵own»ᶻeftĀ;e⠀㜾ñम;扜ightĀ;e㊪㝋ñၚot;旬inus;樺lus;樹b;槍ime;樻ezium;揢ƀcht㝲㝽㞁Āry㝷㝻;쀀𝓉;䑆cy;䑛rok;䅧Āio㞋㞎xô᝷headĀlr㞗㞠eftarro÷ࡏightarrow»ཝऀAHabcdfghlmoprstuw㟐㟓㟗㟤㟰㟼㠎㠜㠣㠴㡑㡝㡫㢩㣌㣒㣪㣶ròϭar;楣Ācr㟜㟢ute耻ú䃺òᅐrǣ㟪\0㟭y;䑞ve;䅭Āiy㟵㟺rc耻û䃻;䑃ƀabh㠃㠆㠋ròᎭlac;䅱aòᏃĀir㠓㠘sht;楾;쀀𝔲rave耻ù䃹š㠧㠱rĀlr㠬㠮»ॗ»ႃlk;斀Āct㠹㡍ɯ㠿\0\0㡊rnĀ;e㡅㡆挜r»㡆op;挏ri;旸Āal㡖㡚cr;䅫肻¨͉Āgp㡢㡦on;䅳f;쀀𝕦̀adhlsuᅋ㡸㡽፲㢑㢠ownáᎳarpoonĀlr㢈㢌efô㠭ighô㠯iƀ;hl㢙㢚㢜䏅»ᏺon»㢚parrows;懈ƀcit㢰㣄㣈ɯ㢶\0\0㣁rnĀ;e㢼㢽挝r»㢽op;挎ng;䅯ri;旹cr;쀀𝓊ƀdir㣙㣝㣢ot;拰lde;䅩iĀ;f㜰㣨»᠓Āam㣯㣲rò㢨l耻ü䃼angle;榧ހABDacdeflnoprsz㤜㤟㤩㤭㦵㦸㦽㧟㧤㧨㧳㧹㧽㨁㨠ròϷarĀ;v㤦㤧櫨;櫩asèϡĀnr㤲㤷grt;榜΀eknprst㓣㥆㥋㥒㥝㥤㦖appá␕othinçẖƀhir㓫⻈㥙opô⾵Ā;hᎷ㥢ïㆍĀiu㥩㥭gmá㎳Ābp㥲㦄setneqĀ;q㥽㦀쀀⊊︀;쀀⫋︀setneqĀ;q㦏㦒쀀⊋︀;쀀⫌︀Āhr㦛㦟etá㚜iangleĀlr㦪㦯eft»थight»ၑy;䐲ash»ံƀelr㧄㧒㧗ƀ;beⷪ㧋㧏ar;抻q;扚lip;拮Ābt㧜ᑨaòᑩr;쀀𝔳tré㦮suĀbp㧯㧱»ജ»൙pf;쀀𝕧roð໻tré㦴Ācu㨆㨋r;쀀𝓋Ābp㨐㨘nĀEe㦀㨖»㥾nĀEe㦒㨞»㦐igzag;榚΀cefoprs㨶㨻㩖㩛㩔㩡㩪irc;䅵Ādi㩀㩑Ābg㩅㩉ar;機eĀ;qᗺ㩏;扙erp;愘r;쀀𝔴pf;쀀𝕨Ā;eᑹ㩦atèᑹcr;쀀𝓌ૣណ㪇\0㪋\0㪐㪛\0\0㪝㪨㪫㪯\0\0㫃㫎\0㫘ៜ៟tré៑r;쀀𝔵ĀAa㪔㪗ròσrò৶;䎾ĀAa㪡㪤ròθrò৫að✓is;拻ƀdptឤ㪵㪾Āfl㪺ឩ;쀀𝕩imåឲĀAa㫇㫊ròώròਁĀcq㫒ីr;쀀𝓍Āpt៖㫜ré។Ѐacefiosu㫰㫽㬈㬌㬑㬕㬛㬡cĀuy㫶㫻te耻ý䃽;䑏Āiy㬂㬆rc;䅷;䑋n耻¥䂥r;쀀𝔶cy;䑗pf;쀀𝕪cr;쀀𝓎Ācm㬦㬩y;䑎l耻ÿ䃿Ԁacdefhiosw㭂㭈㭔㭘㭤㭩㭭㭴㭺㮀cute;䅺Āay㭍㭒ron;䅾;䐷ot;䅼Āet㭝㭡træᕟa;䎶r;쀀𝔷cy;䐶grarr;懝pf;쀀𝕫cr;쀀𝓏Ājn㮅㮇;怍j;怌'.split("").map((e=>e.charCodeAt(0)))),Ad=new Uint16Array("Ȁaglq\tɭ\0\0p;䀦os;䀧t;䀾t;䀼uot;䀢".split("").map((e=>e.charCodeAt(0))));var Id;const Od=new Map([[0,65533],[128,8364],[130,8218],[131,402],[132,8222],[133,8230],[134,8224],[135,8225],[136,710],[137,8240],[138,352],[139,8249],[140,338],[142,381],[145,8216],[146,8217],[147,8220],[148,8221],[149,8226],[150,8211],[151,8212],[152,732],[153,8482],[154,353],[155,8250],[156,339],[158,382],[159,376]]),Ld=null!==(Id=String.fromCodePoint)&&void 0!==Id?Id:function(e){let t="";return e>65535&&(e-=65536,t+=String.fromCharCode(e>>>10&1023|55296),e=56320|1023&e),t+=String.fromCharCode(e),t};var Md,Rd;(Rd=Md||(Md={}))[Rd.NUM=35]="NUM",Rd[Rd.SEMI=59]="SEMI",Rd[Rd.EQUALS=61]="EQUALS",Rd[Rd.ZERO=48]="ZERO",Rd[Rd.NINE=57]="NINE",Rd[Rd.LOWER_A=97]="LOWER_A",Rd[Rd.LOWER_F=102]="LOWER_F",Rd[Rd.LOWER_X=120]="LOWER_X",Rd[Rd.LOWER_Z=122]="LOWER_Z",Rd[Rd.UPPER_A=65]="UPPER_A",Rd[Rd.UPPER_F=70]="UPPER_F",Rd[Rd.UPPER_Z=90]="UPPER_Z";var Fd,Pd,jd,Vd,Bd,$d;function Ud(e){return e>=Md.ZERO&&e<=Md.NINE}function Hd(e){return e===Md.EQUALS||function(e){return e>=Md.UPPER_A&&e<=Md.UPPER_Z||e>=Md.LOWER_A&&e<=Md.LOWER_Z||Ud(e)}(e)}(Pd=Fd||(Fd={}))[Pd.VALUE_LENGTH=49152]="VALUE_LENGTH",Pd[Pd.BRANCH_LENGTH=16256]="BRANCH_LENGTH",Pd[Pd.JUMP_TABLE=127]="JUMP_TABLE",(Vd=jd||(jd={}))[Vd.EntityStart=0]="EntityStart",Vd[Vd.NumericStart=1]="NumericStart",Vd[Vd.NumericDecimal=2]="NumericDecimal",Vd[Vd.NumericHex=3]="NumericHex",Vd[Vd.NamedEntity=4]="NamedEntity",($d=Bd||(Bd={}))[$d.Legacy=0]="Legacy",$d[$d.Strict=1]="Strict",$d[$d.Attribute=2]="Attribute";class qd{constructor(e,t,n){this.decodeTree=e,this.emitCodePoint=t,this.errors=n,this.state=jd.EntityStart,this.consumed=1,this.result=0,this.treeIndex=0,this.excess=1,this.decodeMode=Bd.Strict}startEntity(e){this.decodeMode=e,this.state=jd.EntityStart,this.result=0,this.treeIndex=0,this.excess=1,this.consumed=1}write(e,t){switch(this.state){case jd.EntityStart:return e.charCodeAt(t)===Md.NUM?(this.state=jd.NumericStart,this.consumed+=1,this.stateNumericStart(e,t+1)):(this.state=jd.NamedEntity,this.stateNamedEntity(e,t));case jd.NumericStart:return this.stateNumericStart(e,t);case jd.NumericDecimal:return this.stateNumericDecimal(e,t);case jd.NumericHex:return this.stateNumericHex(e,t);case jd.NamedEntity:return this.stateNamedEntity(e,t)}}stateNumericStart(e,t){return t>=e.length?-1:(32|e.charCodeAt(t))===Md.LOWER_X?(this.state=jd.NumericHex,this.consumed+=1,this.stateNumericHex(e,t+1)):(this.state=jd.NumericDecimal,this.stateNumericDecimal(e,t))}addToNumericResult(e,t,n,r){if(t!==n){const i=n-t;this.result=this.result*Math.pow(r,i)+parseInt(e.substr(t,i),r),this.consumed+=i}}stateNumericHex(e,t){const n=t;for(;t=Md.UPPER_A&&r<=Md.UPPER_F||r>=Md.LOWER_A&&r<=Md.LOWER_F)))return this.addToNumericResult(e,n,t,16),this.emitNumericEntity(i,3);t+=1}var r;return this.addToNumericResult(e,n,t,16),-1}stateNumericDecimal(e,t){const n=t;for(;t=55296&&e<=57343||e>1114111?65533:null!==(t=Od.get(e))&&void 0!==t?t:e}(this.result),this.consumed),this.errors&&(e!==Md.SEMI&&this.errors.missingSemicolonAfterCharacterReference(),this.errors.validateNumericCharacterReference(this.result)),this.consumed}stateNamedEntity(e,t){const{decodeTree:n}=this;let r=n[this.treeIndex],i=(r&Fd.VALUE_LENGTH)>>14;for(;t>14,0!==i){if(o===Md.SEMI)return this.emitNamedEntityData(this.treeIndex,i,this.consumed+this.excess);this.decodeMode!==Bd.Strict&&(this.result=this.treeIndex,this.consumed+=this.excess,this.excess=0)}}return-1}emitNotTerminatedNamedEntity(){var e;const{result:t,decodeTree:n}=this,r=(n[t]&Fd.VALUE_LENGTH)>>14;return this.emitNamedEntityData(t,r,this.consumed),null===(e=this.errors)||void 0===e||e.missingSemicolonAfterCharacterReference(),this.consumed}emitNamedEntityData(e,t,n){const{decodeTree:r}=this;return this.emitCodePoint(1===t?r[e]&~Fd.VALUE_LENGTH:r[e+1],n),3===t&&this.emitCodePoint(r[e+2],n),n}end(){var e;switch(this.state){case jd.NamedEntity:return 0===this.result||this.decodeMode===Bd.Attribute&&this.result!==this.treeIndex?0:this.emitNotTerminatedNamedEntity();case jd.NumericDecimal:return this.emitNumericEntity(0,2);case jd.NumericHex:return this.emitNumericEntity(0,3);case jd.NumericStart:return null===(e=this.errors)||void 0===e||e.absenceOfDigitsInNumericCharacterReference(this.consumed),0;case jd.EntityStart:return 0}}}function Wd(e){let t="";const n=new qd(e,(e=>t+=Ld(e)));return function(e,r){let i=0,o=0;for(;(o=e.indexOf("&",o))>=0;){t+=e.slice(i,o),n.startEntity(r);const s=n.write(e,o+1);if(s<0){i=o+n.end();break}i=o+s,o=0===s?i+1:i}const s=t+e.slice(i);return t="",s}}function zd(e,t,n,r){const i=(t&Fd.BRANCH_LENGTH)>>7,o=t&Fd.JUMP_TABLE;if(0===i)return 0!==o&&r===o?n:-1;if(o){const t=r-o;return t<0||t>=i?-1:e[n+t]-1}let s=n,a=s+i-1;for(;s<=a;){const t=s+a>>>1,n=e[t];if(nr))return e[t+i];a=t-1}}return-1}const Gd=Wd(Dd);function Kd(e,t=Bd.Legacy){return Gd(e,t)}function Yd(e){return"[object String]"===function(e){return Object.prototype.toString.call(e)}(e)}Wd(Ad);const Qd=Object.prototype.hasOwnProperty;function Xd(e){return Array.prototype.slice.call(arguments,1).forEach((function(t){if(t){if("object"!=typeof t)throw new TypeError(t+"must be object");Object.keys(t).forEach((function(n){e[n]=t[n]}))}})),e}function Jd(e,t,n){return[].concat(e.slice(0,t),n,e.slice(t+1))}function Zd(e){return!(e>=55296&&e<=57343)&&(!(e>=64976&&e<=65007)&&(65535!=(65535&e)&&65534!=(65535&e)&&(!(e>=0&&e<=8)&&(11!==e&&(!(e>=14&&e<=31)&&(!(e>=127&&e<=159)&&!(e>1114111)))))))}function ef(e){if(e>65535){const t=55296+((e-=65536)>>10),n=56320+(1023&e);return String.fromCharCode(t,n)}return String.fromCharCode(e)}const tf=/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])/g,nf=new RegExp(tf.source+"|"+/&([a-z#][a-z0-9]{1,31});/gi.source,"gi"),rf=/^#((?:x[a-f0-9]{1,8}|[0-9]{1,8}))$/i;function of(e){return e.indexOf("\\")<0&&e.indexOf("&")<0?e:e.replace(nf,(function(e,t,n){return t||function(e,t){if(35===t.charCodeAt(0)&&rf.test(t)){const n="x"===t[1].toLowerCase()?parseInt(t.slice(2),16):parseInt(t.slice(1),10);return Zd(n)?ef(n):e}const n=Kd(e);return n!==e?n:e}(e,n)}))}const sf=/[&<>"]/,af=/[&<>"]/g,lf={"&":"&","<":"<",">":">",'"':"""};function cf(e){return lf[e]}function uf(e){return sf.test(e)?e.replace(af,cf):e}const df=/[.?*+^$[\]\\(){}|-]/g;function ff(e){switch(e){case 9:case 32:return!0}return!1}function pf(e){if(e>=8192&&e<=8202)return!0;switch(e){case 9:case 10:case 11:case 12:case 13:case 32:case 160:case 5760:case 8239:case 8287:case 12288:return!0}return!1}function hf(e){return Sd.test(e)||kd.test(e)}function mf(e){switch(e){case 33:case 34:case 35:case 36:case 37:case 38:case 39:case 40:case 41:case 42:case 43:case 44:case 45:case 46:case 47:case 58:case 59:case 60:case 61:case 62:case 63:case 64:case 91:case 92:case 93:case 94:case 95:case 96:case 123:case 124:case 125:case 126:return!0;default:return!1}}function gf(e){return e=e.trim().replace(/\s+/g," "),"Ṿ"==="ẞ".toLowerCase()&&(e=e.replace(/ẞ/g,"ß")),e.toLowerCase().toUpperCase()}const vf={mdurl:wd,ucmicro:Nd},yf=Object.freeze(Object.defineProperty({__proto__:null,arrayReplaceAt:Jd,assign:Xd,escapeHtml:uf,escapeRE:function(e){return e.replace(df,"\\$&")},fromCodePoint:ef,has:function(e,t){return Qd.call(e,t)},isMdAsciiPunct:mf,isPunctChar:hf,isSpace:ff,isString:Yd,isValidEntityCode:Zd,isWhiteSpace:pf,lib:vf,normalizeReference:gf,unescapeAll:of,unescapeMd:function(e){return e.indexOf("\\")<0?e:e.replace(tf,"$1")}},Symbol.toStringTag,{value:"Module"}));const bf=Object.freeze(Object.defineProperty({__proto__:null,parseLinkDestination:function(e,t,n){let r,i=t;const o={ok:!1,pos:0,str:""};if(60===e.charCodeAt(i)){for(i++;i32))return o;if(41===r){if(0===s)break;s--}i++}return t===i||0!==s||(o.str=of(e.slice(t,i)),o.pos=i,o.ok=!0),o},parseLinkLabel:function(e,t,n){let r,i,o,s;const a=e.posMax,l=e.pos;for(e.pos=t+1,r=1;e.pos=n)return s;let r=e.charCodeAt(o);if(34!==r&&39!==r&&40!==r)return s;t++,o++,40===r&&(r=41),s.marker=r}for(;o"+uf(o.content)+""},Ef.code_block=function(e,t,n,r,i){const o=e[t];return""+uf(e[t].content)+"\n"},Ef.fence=function(e,t,n,r,i){const o=e[t],s=o.info?of(o.info).trim():"";let a,l="",c="";if(s){const e=s.split(/(\s+)/g);l=e[0],c=e.slice(2).join("")}if(a=n.highlight&&n.highlight(o.content,l,c)||uf(o.content),0===a.indexOf("${a}\n`}return`
${a}
\n`},Ef.image=function(e,t,n,r,i){const o=e[t];return o.attrs[o.attrIndex("alt")][1]=i.renderInlineAsText(o.children,n,r),i.renderToken(e,t,n)},Ef.hardbreak=function(e,t,n){return n.xhtmlOut?"
\n":"
\n"},Ef.softbreak=function(e,t,n){return n.breaks?n.xhtmlOut?"
\n":"
\n":"\n"},Ef.text=function(e,t){return uf(e[t].content)},Ef.html_block=function(e,t){return e[t].content},Ef.html_inline=function(e,t){return e[t].content},xf.prototype.renderAttrs=function(e){let t,n,r;if(!e.attrs)return"";for(r="",t=0,n=e.attrs.length;t\n":">",i},xf.prototype.renderInline=function(e,t,n){let r="";const i=this.rules;for(let o=0,s=e.length;o=0&&(n=this.attrs[t][1]),n},Tf.prototype.attrJoin=function(e,t){const n=this.attrIndex(e);n<0?this.attrPush([e,t]):this.attrs[n][1]=this.attrs[n][1]+" "+t},Cf.prototype.Token=Tf;const Sf=/\r\n?|\n/g,kf=/\0/g;function _f(e){return/^<\/a\s*>/i.test(e)}const Nf=/\+-|\.\.|\?\?\?\?|!!!!|,,|--/,Df=/\((c|tm|r)\)/i,Af=/\((c|tm|r)\)/gi,If={c:"©",r:"®",tm:"™"};function Of(e,t){return If[t.toLowerCase()]}function Lf(e){let t=0;for(let n=e.length-1;n>=0;n--){const r=e[n];"text"!==r.type||t||(r.content=r.content.replace(Af,Of)),"link_open"===r.type&&"auto"===r.info&&t--,"link_close"===r.type&&"auto"===r.info&&t++}}function Mf(e){let t=0;for(let n=e.length-1;n>=0;n--){const r=e[n];"text"!==r.type||t||Nf.test(r.content)&&(r.content=r.content.replace(/\+-/g,"±").replace(/\.{2,}/g,"…").replace(/([?!])…/g,"$1..").replace(/([?!]){4,}/g,"$1$1$1").replace(/,{2,}/g,",").replace(/(^|[^-])---(?=[^-]|$)/gm,"$1—").replace(/(^|\s)--(?=\s|$)/gm,"$1–").replace(/(^|[^-\s])--(?=[^-\s]|$)/gm,"$1–")),"link_open"===r.type&&"auto"===r.info&&t--,"link_close"===r.type&&"auto"===r.info&&t++}}const Rf=/['"]/,Ff=/['"]/g,Pf="’";function jf(e,t,n){return e.slice(0,t)+n+e.slice(t+1)}function Vf(e,t){let n;const r=[];for(let i=0;i=0&&!(r[n].level<=s);n--);if(r.length=n+1,"text"!==o.type)continue;let a=o.content,l=0,c=a.length;e:for(;l=0)h=a.charCodeAt(u.index-1);else for(n=i-1;n>=0&&("softbreak"!==e[n].type&&"hardbreak"!==e[n].type);n--)if(e[n].content){h=e[n].content.charCodeAt(e[n].content.length-1);break}let m=32;if(l=48&&h<=57&&(f=d=!1),d&&f&&(d=g,f=v),d||f){if(f)for(n=r.length-1;n>=0;n--){let d=r[n];if(r[n].level=0;s--){const a=i[s];if("link_close"!==a.type){if("html_inline"===a.type&&(n=a.content,/^\s]/i.test(n)&&o>0&&o--,_f(a.content)&&o++),!(o>0)&&"text"===a.type&&e.md.linkify.test(a.content)){const n=a.content;let o=e.md.linkify.match(n);const l=[];let c=a.level,u=0;o.length>0&&0===o[0].index&&s>0&&"text_special"===i[s-1].type&&(o=o.slice(1));for(let t=0;tu){const t=new e.Token("text","",0);t.content=n.slice(u,a),t.level=c,l.push(t)}const d=new e.Token("link_open","a",1);d.attrs=[["href",i]],d.level=c++,d.markup="linkify",d.info="auto",l.push(d);const f=new e.Token("text","",0);f.content=s,f.level=c,l.push(f);const p=new e.Token("link_close","a",-1);p.level=--c,p.markup="linkify",p.info="auto",l.push(p),u=o[t].lastIndex}if(u=0;t--)"inline"===e.tokens[t].type&&(Df.test(e.tokens[t].content)&&Lf(e.tokens[t].children),Nf.test(e.tokens[t].content)&&Mf(e.tokens[t].children))}],["smartquotes",function(e){if(e.md.options.typographer)for(let t=e.tokens.length-1;t>=0;t--)"inline"===e.tokens[t].type&&Rf.test(e.tokens[t].content)&&Vf(e.tokens[t].children,e)}],["text_join",function(e){let t,n;const r=e.tokens,i=r.length;for(let o=0;o0&&this.level++,this.tokens.push(r),r},Uf.prototype.isEmpty=function(e){return this.bMarks[e]+this.tShift[e]>=this.eMarks[e]},Uf.prototype.skipEmptyLines=function(e){for(let t=this.lineMax;et;)if(!ff(this.src.charCodeAt(--e)))return e+1;return e},Uf.prototype.skipChars=function(e,t){for(let n=this.src.length;en;)if(t!==this.src.charCodeAt(--e))return e+1;return e},Uf.prototype.getLines=function(e,t,n,r){if(e>=t)return"";const i=new Array(t-e);for(let o=0,s=e;sn?new Array(e-n+1).join(" ")+this.src.slice(c,l):this.src.slice(c,l)}return i.join("")},Uf.prototype.Token=Tf;function Hf(e,t){const n=e.bMarks[t]+e.tShift[t],r=e.eMarks[t];return e.src.slice(n,r)}function qf(e){const t=[],n=e.length;let r=0,i=e.charCodeAt(r),o=!1,s=0,a="";for(;r=r)return-1;let o=e.src.charCodeAt(i++);if(o<48||o>57)return-1;for(;;){if(i>=r)return-1;if(o=e.src.charCodeAt(i++),!(o>=48&&o<=57)){if(41===o||46===o)break;return-1}if(i-n>=10)return-1}return i`\\x00-\\x20]+|'[^']*'|\"[^\"]*\"))?)*\\s*\\/?>",Kf="<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>",Yf=new RegExp("^(?:"+Gf+"|"+Kf+"|\x3c!---?>|\x3c!--(?:[^-]|-[^-]|--[^>])*--\x3e|<[?][\\s\\S]*?[?]>|]*>|)"),Qf=new RegExp("^(?:"+Gf+"|"+Kf+")"),Xf=[[/^<(script|pre|style|textarea)(?=(\s|>|$))/i,/<\/(script|pre|style|textarea)>/i,!0],[/^/,!0],[/^<\?/,/\?>/,!0],[/^/,!0],[/^/,!0],[new RegExp("^|$))","i"),/^$/,!0],[new RegExp(Qf.source+"\\s*$"),/^$/,!1]];const Jf=[["table",function(e,t,n,r){if(t+2>n)return!1;let i=t+1;if(e.sCount[i]=4)return!1;let o=e.bMarks[i]+e.tShift[i];if(o>=e.eMarks[i])return!1;const s=e.src.charCodeAt(o++);if(124!==s&&45!==s&&58!==s)return!1;if(o>=e.eMarks[i])return!1;const a=e.src.charCodeAt(o++);if(124!==a&&45!==a&&58!==a&&!ff(a))return!1;if(45===s&&ff(a))return!1;for(;o=4)return!1;c=qf(l),c.length&&""===c[0]&&c.shift(),c.length&&""===c[c.length-1]&&c.pop();const d=c.length;if(0===d||d!==u.length)return!1;if(r)return!0;const f=e.parentType;e.parentType="table";const p=e.md.block.ruler.getRules("blockquote"),h=[t,0];e.push("table_open","table",1).map=h,e.push("thead_open","thead",1).map=[t,t+1],e.push("tr_open","tr",1).map=[t,t+1];for(let v=0;v=4)break;if(c=qf(l),c.length&&""===c[0]&&c.shift(),c.length&&""===c[c.length-1]&&c.pop(),g+=d-c.length,g>65536)break;if(i===t+2){e.push("tbody_open","tbody",1).map=m=[t+2,0]}e.push("tr_open","tr",1).map=[i,i+1];for(let t=0;t=4))break;r++,i=r}e.line=i;const o=e.push("code_block","code",0);return o.content=e.getLines(t,i,4+e.blkIndent,!1)+"\n",o.map=[t,e.line],!0}],["fence",function(e,t,n,r){let i=e.bMarks[t]+e.tShift[t],o=e.eMarks[t];if(e.sCount[t]-e.blkIndent>=4)return!1;if(i+3>o)return!1;const s=e.src.charCodeAt(i);if(126!==s&&96!==s)return!1;let a=i;i=e.skipChars(i,s);let l=i-a;if(l<3)return!1;const c=e.src.slice(a,i),u=e.src.slice(i,o);if(96===s&&u.indexOf(String.fromCharCode(s))>=0)return!1;if(r)return!0;let d=t,f=!1;for(;(d++,!(d>=n))&&(i=a=e.bMarks[d]+e.tShift[d],o=e.eMarks[d],!(i=4||(i=e.skipChars(i,s),i-a=4)return!1;if(62!==e.src.charCodeAt(i))return!1;if(r)return!0;const a=[],l=[],c=[],u=[],d=e.md.block.ruler.getRules("blockquote"),f=e.parentType;e.parentType="blockquote";let p,h=!1;for(p=t;p=o)break;if(62===e.src.charCodeAt(i++)&&!t){let t,n,r=e.sCount[p]+1;32===e.src.charCodeAt(i)?(i++,r++,n=!1,t=!0):9===e.src.charCodeAt(i)?(t=!0,(e.bsCount[p]+r)%4==3?(i++,r++,n=!1):n=!0):t=!1;let s=r;for(a.push(e.bMarks[p]),e.bMarks[p]=i;i=o,l.push(e.bsCount[p]),e.bsCount[p]=e.sCount[p]+1+(t?1:0),c.push(e.sCount[p]),e.sCount[p]=s-r,u.push(e.tShift[p]),e.tShift[p]=i-e.bMarks[p];continue}if(h)break;let r=!1;for(let i=0,o=d.length;i";const v=[t,0];g.map=v,e.md.block.tokenize(e,t,p),e.push("blockquote_close","blockquote",-1).markup=">",e.lineMax=s,e.parentType=f,v[1]=e.line;for(let y=0;y=4)return!1;let o=e.bMarks[t]+e.tShift[t];const s=e.src.charCodeAt(o++);if(42!==s&&45!==s&&95!==s)return!1;let a=1;for(;o=4)return!1;if(e.listIndent>=0&&e.sCount[l]-e.listIndent>=4&&e.sCount[l]=e.blkIndent&&(p=!0),(f=zf(e,l))>=0){if(u=!0,s=e.bMarks[l]+e.tShift[l],d=Number(e.src.slice(s,f-1)),p&&1!==d)return!1}else{if(!((f=Wf(e,l))>=0))return!1;u=!1}if(p&&e.skipSpaces(f)>=e.eMarks[l])return!1;if(r)return!0;const h=e.src.charCodeAt(f-1),m=e.tokens.length;u?(a=e.push("ordered_list_open","ol",1),1!==d&&(a.attrs=[["start",d]])):a=e.push("bullet_list_open","ul",1);const g=[l,0];a.map=g,a.markup=String.fromCharCode(h);let v=!1;const y=e.md.block.ruler.getRules("list"),b=e.parentType;for(e.parentType="list";l=i?1:r-t,p>4&&(p=1);const m=t+p;a=e.push("list_item_open","li",1),a.markup=String.fromCharCode(h);const g=[l,0];a.map=g,u&&(a.info=e.src.slice(s,f-1));const b=e.tight,E=e.tShift[l],x=e.sCount[l],w=e.listIndent;if(e.listIndent=e.blkIndent,e.blkIndent=m,e.tight=!0,e.tShift[l]=d-e.bMarks[l],e.sCount[l]=r,d>=i&&e.isEmpty(l+1)?e.line=Math.min(e.line+2,n):e.md.block.tokenize(e,l,n,!0),e.tight&&!v||(c=!1),v=e.line-l>1&&e.isEmpty(e.line-1),e.blkIndent=e.listIndent,e.listIndent=w,e.tShift[l]=E,e.sCount[l]=x,e.tight=b,a=e.push("list_item_close","li",-1),a.markup=String.fromCharCode(h),l=e.line,g[1]=l,l>=n)break;if(e.sCount[l]=4)break;let T=!1;for(let i=0,o=y.length;i=4)return!1;if(91!==e.src.charCodeAt(i))return!1;function a(t){const n=e.lineMax;if(t>=n||e.isEmpty(t))return null;let r=!1;if(e.sCount[t]-e.blkIndent>3&&(r=!0),e.sCount[t]<0&&(r=!0),!r){const r=e.md.block.ruler.getRules("reference"),i=e.parentType;e.parentType="reference";let o=!1;for(let s=0,a=r.length;s=4)return!1;if(!e.md.options.html)return!1;if(60!==e.src.charCodeAt(i))return!1;let s=e.src.slice(i,o),a=0;for(;a=4)return!1;let s=e.src.charCodeAt(i);if(35!==s||i>=o)return!1;let a=1;for(s=e.src.charCodeAt(++i);35===s&&i6||ii&&ff(e.src.charCodeAt(l-1))&&(o=l),e.line=t+1;const c=e.push("heading_open","h"+String(a),1);c.markup="########".slice(0,a),c.map=[t,e.line];const u=e.push("inline","",0);return u.content=e.src.slice(i,o).trim(),u.map=[t,e.line],u.children=[],e.push("heading_close","h"+String(a),-1).markup="########".slice(0,a),!0},["paragraph","reference","blockquote"]],["lheading",function(e,t,n){const r=e.md.block.ruler.getRules("paragraph");if(e.sCount[t]-e.blkIndent>=4)return!1;const i=e.parentType;e.parentType="paragraph";let o,s=0,a=t+1;for(;a3)continue;if(e.sCount[a]>=e.blkIndent){let t=e.bMarks[a]+e.tShift[a];const n=e.eMarks[a];if(t=n))){s=61===o?1:2;break}}if(e.sCount[a]<0)continue;let t=!1;for(let i=0,o=r.length;i3)continue;if(e.sCount[o]<0)continue;let t=!1;for(let i=0,s=r.length;i=n))&&!(e.sCount[s]=o){e.line=n;break}const t=e.line;let l=!1;for(let o=0;o=e.line)throw new Error("block rule didn't increment state.line");break}if(!l)throw new Error("none of the block rules matched");e.tight=!a,e.isEmpty(e.line-1)&&(a=!0),s=e.line,s0&&(this.level++,this._prev_delimiters.push(this.delimiters),this.delimiters=[],i={delimiters:this.delimiters}),this.pendingLevel=this.level,this.tokens.push(r),this.tokens_meta.push(i),r},ep.prototype.scanDelims=function(e,t){const n=this.posMax,r=this.src.charCodeAt(e),i=e>0?this.src.charCodeAt(e-1):32;let o=e;for(;o?@[]^_`{|}~-".split("").forEach((function(e){rp[e.charCodeAt(0)]=1}));const op={tokenize:function(e,t){const n=e.pos,r=e.src.charCodeAt(n);if(t)return!1;if(126!==r)return!1;const i=e.scanDelims(e.pos,!0);let o=i.length;const s=String.fromCharCode(r);if(o<2)return!1;let a;o%2&&(a=e.push("text","",0),a.content=s,o--);for(let l=0;l=0;n--){const r=t[n];if(95!==r.marker&&42!==r.marker)continue;if(-1===r.end)continue;const i=t[r.end],o=n>0&&t[n-1].end===r.end+1&&t[n-1].marker===r.marker&&t[n-1].token===r.token-1&&t[r.end+1].token===i.token+1,s=String.fromCharCode(r.marker),a=e.tokens[r.token];a.type=o?"strong_open":"em_open",a.tag=o?"strong":"em",a.nesting=1,a.markup=o?s+s:s,a.content="";const l=e.tokens[i.token];l.type=o?"strong_close":"em_close",l.tag=o?"strong":"em",l.nesting=-1,l.markup=o?s+s:s,l.content="",o&&(e.tokens[t[n-1].token].content="",e.tokens[t[r.end+1].token].content="",n--)}}const ap={tokenize:function(e,t){const n=e.pos,r=e.src.charCodeAt(n);if(t)return!1;if(95!==r&&42!==r)return!1;const i=e.scanDelims(e.pos,42===r);for(let o=0;o\x00-\x20]*)$/;const up=/^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));/i,dp=/^&([a-z][a-z0-9]{1,31});/i;function fp(e){const t={},n=e.length;if(!n)return;let r=0,i=-2;const o=[];for(let s=0;sa;l-=o[l]+1){const t=e[l];if(t.marker===n.marker&&(t.open&&t.end<0)){let r=!1;if((t.close||n.open)&&(t.length+n.length)%3==0&&(t.length%3==0&&n.length%3==0||(r=!0)),!r){const r=l>0&&!e[l-1].open?o[l-1]+1:0;o[s]=s-l+r,o[l]=r,n.open=!1,t.end=s,t.close=!1,c=-1,i=-2;break}}}-1!==c&&(t[n.marker][(n.open?3:0)+(n.length||0)%3]=c)}}const pp=[["text",function(e,t){let n=e.pos;for(;n0)return!1;const n=e.pos;if(n+3>e.posMax)return!1;if(58!==e.src.charCodeAt(n))return!1;if(47!==e.src.charCodeAt(n+1))return!1;if(47!==e.src.charCodeAt(n+2))return!1;const r=e.pending.match(np);if(!r)return!1;const i=r[1],o=e.md.linkify.matchAtStart(e.src.slice(n-i.length));if(!o)return!1;let s=o.url;if(s.length<=i.length)return!1;s=s.replace(/\*+$/,"");const a=e.md.normalizeLink(s);if(!e.md.validateLink(a))return!1;if(!t){e.pending=e.pending.slice(0,-i.length);const t=e.push("link_open","a",1);t.attrs=[["href",a]],t.markup="linkify",t.info="auto";e.push("text","",0).content=e.md.normalizeLinkText(s);const n=e.push("link_close","a",-1);n.markup="linkify",n.info="auto"}return e.pos+=s.length-i.length,!0}],["newline",function(e,t){let n=e.pos;if(10!==e.src.charCodeAt(n))return!1;const r=e.pending.length-1,i=e.posMax;if(!t)if(r>=0&&32===e.pending.charCodeAt(r))if(r>=1&&32===e.pending.charCodeAt(r-1)){let t=r-1;for(;t>=1&&32===e.pending.charCodeAt(t-1);)t--;e.pending=e.pending.slice(0,t),e.push("hardbreak","br",0)}else e.pending=e.pending.slice(0,-1),e.push("softbreak","br",0);else e.push("softbreak","br",0);for(n++;n=r)return!1;let i=e.src.charCodeAt(n);if(10===i){for(t||e.push("hardbreak","br",0),n++;n=55296&&i<=56319&&n+1=56320&&t<=57343&&(o+=e.src[n+1],n++)}const s="\\"+o;if(!t){const t=e.push("text_special","",0);i<256&&0!==rp[i]?t.content=o:t.content=s,t.markup=s,t.info="escape"}return e.pos=n+1,!0}],["backticks",function(e,t){let n=e.pos;if(96!==e.src.charCodeAt(n))return!1;const r=n;n++;const i=e.posMax;for(;n=d)return!1;if(l=h,i=e.md.helpers.parseLinkDestination(e.src,h,e.posMax),i.ok){for(s=e.md.normalizeLink(i.str),e.md.validateLink(s)?h=i.pos:s="",l=h;h=d||41!==e.src.charCodeAt(h))&&(c=!0),h++}if(c){if(void 0===e.env.references)return!1;if(h=0?r=e.src.slice(l,h++):h=p+1):h=p+1,r||(r=e.src.slice(f,p)),o=e.env.references[gf(r)],!o)return e.pos=u,!1;s=o.href,a=o.title}if(!t){e.pos=f,e.posMax=p;const t=[["href",s]];e.push("link_open","a",1).attrs=t,a&&t.push(["title",a]),e.linkLevel++,e.md.inline.tokenize(e),e.linkLevel--,e.push("link_close","a",-1)}return e.pos=h,e.posMax=d,!0}],["image",function(e,t){let n,r,i,o,s,a,l,c,u="";const d=e.pos,f=e.posMax;if(33!==e.src.charCodeAt(e.pos))return!1;if(91!==e.src.charCodeAt(e.pos+1))return!1;const p=e.pos+2,h=e.md.helpers.parseLinkLabel(e,e.pos+1,!1);if(h<0)return!1;if(o=h+1,o=f)return!1;for(c=o,a=e.md.helpers.parseLinkDestination(e.src,o,e.posMax),a.ok&&(u=e.md.normalizeLink(a.str),e.md.validateLink(u)?o=a.pos:u=""),c=o;o=f||41!==e.src.charCodeAt(o))return e.pos=d,!1;o++}else{if(void 0===e.env.references)return!1;if(o=0?i=e.src.slice(c,o++):o=h+1):o=h+1,i||(i=e.src.slice(p,h)),s=e.env.references[gf(i)],!s)return e.pos=d,!1;u=s.href,l=s.title}if(!t){r=e.src.slice(p,h);const t=[];e.md.inline.parse(r,e.md,e.env,t);const n=e.push("image","img",0),i=[["src",u],["alt",""]];n.attrs=i,n.children=t,n.content=r,l&&i.push(["title",l])}return e.pos=o,e.posMax=f,!0}],["autolink",function(e,t){let n=e.pos;if(60!==e.src.charCodeAt(n))return!1;const r=e.pos,i=e.posMax;for(;;){if(++n>=i)return!1;const t=e.src.charCodeAt(n);if(60===t)return!1;if(62===t)break}const o=e.src.slice(r+1,n);if(cp.test(o)){const n=e.md.normalizeLink(o);if(!e.md.validateLink(n))return!1;if(!t){const t=e.push("link_open","a",1);t.attrs=[["href",n]],t.markup="autolink",t.info="auto";e.push("text","",0).content=e.md.normalizeLinkText(o);const r=e.push("link_close","a",-1);r.markup="autolink",r.info="auto"}return e.pos+=o.length+2,!0}if(lp.test(o)){const n=e.md.normalizeLink("mailto:"+o);if(!e.md.validateLink(n))return!1;if(!t){const t=e.push("link_open","a",1);t.attrs=[["href",n]],t.markup="autolink",t.info="auto";e.push("text","",0).content=e.md.normalizeLinkText(o);const r=e.push("link_close","a",-1);r.markup="autolink",r.info="auto"}return e.pos+=o.length+2,!0}return!1}],["html_inline",function(e,t){if(!e.md.options.html)return!1;const n=e.posMax,r=e.pos;if(60!==e.src.charCodeAt(r)||r+2>=n)return!1;const i=e.src.charCodeAt(r+1);if(33!==i&&63!==i&&47!==i&&!function(e){const t=32|e;return t>=97&&t<=122}(i))return!1;const o=e.src.slice(r).match(Yf);if(!o)return!1;if(!t){const t=e.push("html_inline","",0);t.content=o[0],s=t.content,/^\s]/i.test(s)&&e.linkLevel++,function(e){return/^<\/a\s*>/i.test(e)}(t.content)&&e.linkLevel--}var s;return e.pos+=o[0].length,!0}],["entity",function(e,t){const n=e.pos,r=e.posMax;if(38!==e.src.charCodeAt(n))return!1;if(n+1>=r)return!1;if(35===e.src.charCodeAt(n+1)){const r=e.src.slice(n).match(up);if(r){if(!t){const t="x"===r[1][0].toLowerCase()?parseInt(r[1].slice(1),16):parseInt(r[1],10),n=e.push("text_special","",0);n.content=Zd(t)?ef(t):ef(65533),n.markup=r[0],n.info="entity"}return e.pos+=r[0].length,!0}}else{const r=e.src.slice(n).match(dp);if(r){const n=Kd(r[0]);if(n!==r[0]){if(!t){const t=e.push("text_special","",0);t.content=n,t.markup=r[0],t.info="entity"}return e.pos+=r[0].length,!0}}}return!1}]],hp=[["balance_pairs",function(e){const t=e.tokens_meta,n=e.tokens_meta.length;fp(e.delimiters);for(let r=0;r0&&r++,"text"===i[t].type&&t+1=e.pos)throw new Error("inline rule didn't increment state.pos");break}}else e.pos=e.posMax;s||e.pos++,o[t]=e.pos},mp.prototype.tokenize=function(e){const t=this.ruler.getRules(""),n=t.length,r=e.posMax,i=e.md.options.maxNesting;for(;e.pos=e.pos)throw new Error("inline rule didn't increment state.pos");break}if(s){if(e.pos>=r)break}else e.pending+=e.src[e.pos++]}e.pending&&e.pushPending()},mp.prototype.parse=function(e,t,n,r){const i=new this.State(e,t,n,r);this.tokenize(i);const o=this.ruler2.getRules(""),s=o.length;for(let a=0;a=3&&":"===e[t-3]||t>=3&&"/"===e[t-3]?0:r.match(n.re.no_http)[0].length:0}},"mailto:":{validate:function(e,t,n){const r=e.slice(t);return n.re.mailto||(n.re.mailto=new RegExp("^"+n.re.src_email_name+"@"+n.re.src_host_strict,"i")),n.re.mailto.test(r)?r.match(n.re.mailto)[0].length:0}}},wp="a[cdefgilmnoqrstuwxz]|b[abdefghijmnorstvwyz]|c[acdfghiklmnoruvwxyz]|d[ejkmoz]|e[cegrstu]|f[ijkmor]|g[abdefghilmnpqrstuwy]|h[kmnrtu]|i[delmnoqrst]|j[emop]|k[eghimnprwyz]|l[abcikrstuvy]|m[acdeghklmnopqrstuvwxyz]|n[acefgilopruz]|om|p[aefghklmnrstwy]|qa|r[eosuw]|s[abcdeghijklmnortuvxyz]|t[cdfghjklmnortvwz]|u[agksyz]|v[aceginu]|w[fs]|y[et]|z[amw]",Tp="biz|com|edu|gov|net|org|pro|web|xxx|aero|asia|coop|info|museum|name|shop|рф".split("|");function Cp(e){const t=e.re=function(e){const t={};e=e||{},t.src_Any=Td.source,t.src_Cc=Cd.source,t.src_Z=_d.source,t.src_P=Sd.source,t.src_ZPCc=[t.src_Z,t.src_P,t.src_Cc].join("|"),t.src_ZCc=[t.src_Z,t.src_Cc].join("|");const n="[><|]";return t.src_pseudo_letter="(?:(?![><|]|"+t.src_ZPCc+")"+t.src_Any+")",t.src_ip4="(?:(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)",t.src_auth="(?:(?:(?!"+t.src_ZCc+"|[@/\\[\\]()]).)+@)?",t.src_port="(?::(?:6(?:[0-4]\\d{3}|5(?:[0-4]\\d{2}|5(?:[0-2]\\d|3[0-5])))|[1-5]?\\d{1,4}))?",t.src_host_terminator="(?=$|[><|]|"+t.src_ZPCc+")(?!"+(e["---"]?"-(?!--)|":"-|")+"_|:\\d|\\.-|\\.(?!$|"+t.src_ZPCc+"))",t.src_path="(?:[/?#](?:(?!"+t.src_ZCc+"|"+n+"|[()[\\]{}.,\"'?!\\-;]).|\\[(?:(?!"+t.src_ZCc+"|\\]).)*\\]|\\((?:(?!"+t.src_ZCc+"|[)]).)*\\)|\\{(?:(?!"+t.src_ZCc+'|[}]).)*\\}|\\"(?:(?!'+t.src_ZCc+'|["]).)+\\"|\\\'(?:(?!'+t.src_ZCc+"|[']).)+\\'|\\'(?="+t.src_pseudo_letter+"|[-])|\\.{2,}[a-zA-Z0-9%/&]|\\.(?!"+t.src_ZCc+"|[.]|$)|"+(e["---"]?"\\-(?!--(?:[^-]|$))(?:-*)|":"\\-+|")+",(?!"+t.src_ZCc+"|$)|;(?!"+t.src_ZCc+"|$)|\\!+(?!"+t.src_ZCc+"|[!]|$)|\\?(?!"+t.src_ZCc+"|[?]|$))+|\\/)?",t.src_email_name='[\\-;:&=\\+\\$,\\.a-zA-Z0-9_][\\-;:&=\\+\\$,\\"\\.a-zA-Z0-9_]*',t.src_xn="xn--[a-z0-9\\-]{1,59}",t.src_domain_root="(?:"+t.src_xn+"|"+t.src_pseudo_letter+"{1,63})",t.src_domain="(?:"+t.src_xn+"|(?:"+t.src_pseudo_letter+")|(?:"+t.src_pseudo_letter+"(?:-|"+t.src_pseudo_letter+"){0,61}"+t.src_pseudo_letter+"))",t.src_host="(?:(?:(?:(?:"+t.src_domain+")\\.)*"+t.src_domain+"))",t.tpl_host_fuzzy="(?:"+t.src_ip4+"|(?:(?:(?:"+t.src_domain+")\\.)+(?:%TLDS%)))",t.tpl_host_no_ip_fuzzy="(?:(?:(?:"+t.src_domain+")\\.)+(?:%TLDS%))",t.src_host_strict=t.src_host+t.src_host_terminator,t.tpl_host_fuzzy_strict=t.tpl_host_fuzzy+t.src_host_terminator,t.src_host_port_strict=t.src_host+t.src_port+t.src_host_terminator,t.tpl_host_port_fuzzy_strict=t.tpl_host_fuzzy+t.src_port+t.src_host_terminator,t.tpl_host_port_no_ip_fuzzy_strict=t.tpl_host_no_ip_fuzzy+t.src_port+t.src_host_terminator,t.tpl_host_fuzzy_test="localhost|www\\.|\\.\\d{1,3}\\.|(?:\\.(?:%TLDS%)(?:"+t.src_ZPCc+"|>|$))",t.tpl_email_fuzzy='(^|[><|]|"|\\(|'+t.src_ZCc+")("+t.src_email_name+"@"+t.tpl_host_fuzzy_strict+")",t.tpl_link_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`||]|"+t.src_ZPCc+"))((?![$+<=>^`||])"+t.tpl_host_port_fuzzy_strict+t.src_path+")",t.tpl_link_no_ip_fuzzy="(^|(?![.:/\\-_@])(?:[$+<=>^`||]|"+t.src_ZPCc+"))((?![$+<=>^`||])"+t.tpl_host_port_no_ip_fuzzy_strict+t.src_path+")",t}(e.__opts__),n=e.__tlds__.slice();function r(e){return e.replace("%TLDS%",t.src_tlds)}e.onCompile(),e.__tlds_replaced__||n.push(wp),n.push(t.src_xn),t.src_tlds=n.join("|"),t.email_fuzzy=RegExp(r(t.tpl_email_fuzzy),"i"),t.link_fuzzy=RegExp(r(t.tpl_link_fuzzy),"i"),t.link_no_ip_fuzzy=RegExp(r(t.tpl_link_no_ip_fuzzy),"i"),t.host_fuzzy_test=RegExp(r(t.tpl_host_fuzzy_test),"i");const i=[];function o(e,t){throw new Error('(LinkifyIt) Invalid schema "'+e+'": '+t)}e.__compiled__={},Object.keys(e.__schemas__).forEach((function(t){const n=e.__schemas__[t];if(null===n)return;const r={validate:null,link:null};if(e.__compiled__[t]=r,"[object Object]"===vp(n))return!function(e){return"[object RegExp]"===vp(e)}(n.validate)?yp(n.validate)?r.validate=n.validate:o(t,n):r.validate=function(e){return function(t,n){const r=t.slice(n);return e.test(r)?r.match(e)[0].length:0}}(n.validate),void(yp(n.normalize)?r.normalize=n.normalize:n.normalize?o(t,n):r.normalize=function(e,t){t.normalize(e)});!function(e){return"[object String]"===vp(e)}(n)?o(t,n):i.push(t)})),i.forEach((function(t){e.__compiled__[e.__schemas__[t]]&&(e.__compiled__[t].validate=e.__compiled__[e.__schemas__[t]].validate,e.__compiled__[t].normalize=e.__compiled__[e.__schemas__[t]].normalize)})),e.__compiled__[""]={validate:null,normalize:function(e,t){t.normalize(e)}};const s=Object.keys(e.__compiled__).filter((function(t){return t.length>0&&e.__compiled__[t]})).map(bp).join("|");e.re.schema_test=RegExp("(^|(?!_)(?:[><|]|"+t.src_ZPCc+"))("+s+")","i"),e.re.schema_search=RegExp("(^|(?!_)(?:[><|]|"+t.src_ZPCc+"))("+s+")","ig"),e.re.schema_at_start=RegExp("^"+e.re.schema_search.source,"i"),e.re.pretest=RegExp("("+e.re.schema_test.source+")|("+e.re.host_fuzzy_test.source+")|@","i"),function(e){e.__index__=-1,e.__text_cache__=""}(e)}function Sp(e,t){const n=e.__index__,r=e.__last_index__,i=e.__text_cache__.slice(n,r);this.schema=e.__schema__.toLowerCase(),this.index=n+t,this.lastIndex=r+t,this.raw=i,this.text=i,this.url=i}function kp(e,t){const n=new Sp(e,t);return e.__compiled__[n.schema].normalize(n,e),n}function _p(e,t){if(!(this instanceof _p))return new _p(e,t);var n;t||(n=e,Object.keys(n||{}).reduce((function(e,t){return e||Ep.hasOwnProperty(t)}),!1)&&(t=e,e={})),this.__opts__=gp({},Ep,t),this.__index__=-1,this.__last_index__=-1,this.__schema__="",this.__text_cache__="",this.__schemas__=gp({},xp,e),this.__compiled__={},this.__tlds__=Tp,this.__tlds_replaced__=!1,this.re={},Cp(this)}_p.prototype.add=function(e,t){return this.__schemas__[e]=t,Cp(this),this},_p.prototype.set=function(e){return this.__opts__=gp(this.__opts__,e),this},_p.prototype.test=function(e){if(this.__text_cache__=e,this.__index__=-1,!e.length)return!1;let t,n,r,i,o,s,a,l,c;if(this.re.schema_test.test(e))for(a=this.re.schema_search,a.lastIndex=0;null!==(t=a.exec(e));)if(i=this.testSchemaAt(e,t[2],a.lastIndex),i){this.__schema__=t[2],this.__index__=t.index+t[1].length,this.__last_index__=t.index+t[0].length+i;break}return this.__opts__.fuzzyLink&&this.__compiled__["http:"]&&(l=e.search(this.re.host_fuzzy_test),l>=0&&(this.__index__<0||l=0&&null!==(r=e.match(this.re.email_fuzzy))&&(o=r.index+r[1].length,s=r.index+r[0].length,(this.__index__<0||othis.__last_index__)&&(this.__schema__="mailto:",this.__index__=o,this.__last_index__=s))),this.__index__>=0},_p.prototype.pretest=function(e){return this.re.pretest.test(e)},_p.prototype.testSchemaAt=function(e,t,n){return this.__compiled__[t.toLowerCase()]?this.__compiled__[t.toLowerCase()].validate(e,n,this):0},_p.prototype.match=function(e){const t=[];let n=0;this.__index__>=0&&this.__text_cache__===e&&(t.push(kp(this,n)),n=this.__last_index__);let r=n?e.slice(n):e;for(;this.test(r);)t.push(kp(this,n)),r=r.slice(this.__last_index__),n+=this.__last_index__;return t.length?t:null},_p.prototype.matchAtStart=function(e){if(this.__text_cache__=e,this.__index__=-1,!e.length)return null;const t=this.re.schema_at_start.exec(e);if(!t)return null;const n=this.testSchemaAt(e,t[2],t[0].length);return n?(this.__schema__=t[2],this.__index__=t.index+t[1].length,this.__last_index__=t.index+t[0].length+n,kp(this,0)):null},_p.prototype.tlds=function(e,t){return e=Array.isArray(e)?e:[e],t?(this.__tlds__=this.__tlds__.concat(e).sort().filter((function(e,t,n){return e!==n[t-1]})).reverse(),Cp(this),this):(this.__tlds__=e.slice(),this.__tlds_replaced__=!0,Cp(this),this)},_p.prototype.normalize=function(e){e.schema||(e.url="http://"+e.url),"mailto:"!==e.schema||/^mailto:/i.test(e.url)||(e.url="mailto:"+e.url)},_p.prototype.onCompile=function(){};const Np=2147483647,Dp=36,Ap=/^xn--/,Ip=/[^\0-\x7F]/,Op=/[\x2E\u3002\uFF0E\uFF61]/g,Lp={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},Mp=Math.floor,Rp=String.fromCharCode;function Fp(e){throw new RangeError(Lp[e])}function Pp(e,t){const n=e.split("@");let r="";n.length>1&&(r=n[0]+"@",e=n[1]);const i=function(e,t){const n=[];let r=e.length;for(;r--;)n[r]=t(e[r]);return n}((e=e.replace(Op,".")).split("."),t).join(".");return r+i}function jp(e){const t=[];let n=0;const r=e.length;for(;n=55296&&i<=56319&&n>1,e+=Mp(e/t);e>455;r+=Dp)e=Mp(e/35);return Mp(r+36*e/(e+38))},$p=function(e){const t=[],n=e.length;let r=0,i=128,o=72,s=e.lastIndexOf("-");s<0&&(s=0);for(let l=0;l=128&&Fp("not-basic"),t.push(e.charCodeAt(l));for(let l=s>0?s+1:0;l=n&&Fp("invalid-input");const s=(a=e.charCodeAt(l++))>=48&&a<58?a-48+26:a>=65&&a<91?a-65:a>=97&&a<123?a-97:Dp;s>=Dp&&Fp("invalid-input"),s>Mp((Np-r)/t)&&Fp("overflow"),r+=s*t;const c=i<=o?1:i>=o+26?26:i-o;if(sMp(Np/u)&&Fp("overflow"),t*=u}const c=t.length+1;o=Bp(r-s,c,0==s),Mp(r/c)>Np-i&&Fp("overflow"),i+=Mp(r/c),r%=c,t.splice(r++,0,i)}var a;return String.fromCodePoint(...t)},Up=function(e){const t=[],n=(e=jp(e)).length;let r=128,i=0,o=72;for(const l of e)l<128&&t.push(Rp(l));const s=t.length;let a=s;for(s&&t.push("-");a=r&&tMp((Np-i)/l)&&Fp("overflow"),i+=(n-r)*l,r=n;for(const c of e)if(cNp&&Fp("overflow"),c===r){let e=i;for(let n=Dp;;n+=Dp){const r=n<=o?1:n>=o+26?26:n-o;if(eString.fromCodePoint(...e)},decode:$p,encode:Up,toASCII:function(e){return Pp(e,(function(e){return Ip.test(e)?"xn--"+Up(e):e}))},toUnicode:function(e){return Pp(e,(function(e){return Ap.test(e)?$p(e.slice(4).toLowerCase()):e}))}},qp={default:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:100},components:{core:{},block:{},inline:{}}},zero:{options:{html:!1,xhtmlOut:!1,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["paragraph"]},inline:{rules:["text"],rules2:["balance_pairs","fragments_join"]}}},commonmark:{options:{html:!0,xhtmlOut:!0,breaks:!1,langPrefix:"language-",linkify:!1,typographer:!1,quotes:"“”‘’",highlight:null,maxNesting:20},components:{core:{rules:["normalize","block","inline","text_join"]},block:{rules:["blockquote","code","fence","heading","hr","html_block","lheading","list","reference","paragraph"]},inline:{rules:["autolink","backticks","emphasis","entity","escape","html_inline","image","link","newline","text"],rules2:["balance_pairs","emphasis","fragments_join"]}}}},Wp=/^(vbscript|javascript|file|data):/,zp=/^data:image\/(gif|png|jpeg|webp);/;function Gp(e){const t=e.trim().toLowerCase();return!Wp.test(t)||zp.test(t)}const Kp=["http:","https:","mailto:"];function Yp(e){const t=xd(e,!0);if(t.hostname&&(!t.protocol||Kp.indexOf(t.protocol)>=0))try{t.hostname=Hp.toASCII(t.hostname)}catch(n){}return ad(ld(t))}function Qp(e){const t=xd(e,!0);if(t.hostname&&(!t.protocol||Kp.indexOf(t.protocol)>=0))try{t.hostname=Hp.toUnicode(t.hostname)}catch(n){}return od(ld(t),od.defaultChars+"%")}function Xp(e,t){if(!(this instanceof Xp))return new Xp(e,t);t||Yd(e)||(t=e||{},e="default"),this.inline=new mp,this.block=new Zf,this.core=new $f,this.renderer=new xf,this.linkify=new _p,this.validateLink=Gp,this.normalizeLink=Yp,this.normalizeLinkText=Qp,this.utils=yf,this.helpers=Xd({},bf),this.options={},this.configure(e),t&&this.set(t)}Xp.prototype.set=function(e){return Xd(this.options,e),this},Xp.prototype.configure=function(e){const t=this;if(Yd(e)){const t=e;if(!(e=qp[t]))throw new Error('Wrong `markdown-it` preset "'+t+'", check name')}if(!e)throw new Error("Wrong `markdown-it` preset, can't be empty");return e.options&&t.set(e.options),e.components&&Object.keys(e.components).forEach((function(n){e.components[n].rules&&t[n].ruler.enableOnly(e.components[n].rules),e.components[n].rules2&&t[n].ruler2.enableOnly(e.components[n].rules2)})),this},Xp.prototype.enable=function(e,t){let n=[];Array.isArray(e)||(e=[e]),["core","block","inline"].forEach((function(t){n=n.concat(this[t].ruler.enable(e,!0))}),this),n=n.concat(this.inline.ruler2.enable(e,!0));const r=e.filter((function(e){return n.indexOf(e)<0}));if(r.length&&!t)throw new Error("MarkdownIt. Failed to enable unknown rule(s): "+r);return this},Xp.prototype.disable=function(e,t){let n=[];Array.isArray(e)||(e=[e]),["core","block","inline"].forEach((function(t){n=n.concat(this[t].ruler.disable(e,!0))}),this),n=n.concat(this.inline.ruler2.disable(e,!0));const r=e.filter((function(e){return n.indexOf(e)<0}));if(r.length&&!t)throw new Error("MarkdownIt. Failed to disable unknown rule(s): "+r);return this},Xp.prototype.use=function(e){const t=[this].concat(Array.prototype.slice.call(arguments,1));return e.apply(e,t),this},Xp.prototype.parse=function(e,t){if("string"!=typeof e)throw new Error("Input data should be a String");const n=new this.core.State(e,this,t);return this.core.process(n),n.tokens},Xp.prototype.render=function(e,t){return t=t||{},this.renderer.render(this.parse(e,t),this.options,t)},Xp.prototype.parseInline=function(e,t){const n=new this.core.State(e,this,t);return n.inlineMode=!0,this.core.process(n),n.tokens},Xp.prototype.renderInline=function(e,t){return t=t||{},this.renderer.render(this.parseInline(e,t),this.options,t)};const Jp=new Xp({breaks:!1,linkify:!0});function Zp(e,t){let n;return function(...r){n&&clearTimeout(n),n=setTimeout((()=>{n=null,t(...r)}),e)}}function eh(t,n){const r=h.c(4);let i,o;r[0]!==t||r[1]!==n?(i=()=>{t&&"string"==typeof n&&n!==t.getValue()&&t.setValue(n)},o=[t,n],r[0]=t,r[1]=n,r[2]=i,r[3]=o):(i=r[2],o=r[3]),e.useEffect(i,o)}function th(t,n,r){const i=h.c(5);let o,s;i[0]!==t||i[1]!==n||i[2]!==r?(o=()=>{null==t||t.setOption(n,r)},s=[t,n,r],i[0]=t,i[1]=n,i[2]=r,i[3]=o,i[4]=s):(o=i[3],s=i[4]),e.useEffect(o,s)}function nh(t,n,r,i,o){const s=h.c(9);let a;s[0]!==o?(a={nonNull:!0,caller:o},s[0]=o,s[1]=a):a=s[1];const{updateActiveTabValues:l}=Qh(a);let c,u;s[2]!==n||s[3]!==t||s[4]!==r||s[5]!==i||s[6]!==l?(c=()=>{if(!t)return;const{storage:e}=Mu.getState(),o=Zp(500,(t=>{null!==r&&e.set(r,t)})),s=Zp(100,(e=>{l({[i]:e})})),a=(e,t)=>{if(!t)return;const r=e.getValue();o(r),s(r),null==n||n(r)};return t.on("change",a),()=>t.off("change",a)},u=[n,t,r,i,l],s[2]=n,s[3]=t,s[4]=r,s[5]=i,s[6]=l,s[7]=c,s[8]=u):(c=s[7],u=s[8]),e.useEffect(c,u)}function rh(t,n){const r=h.c(7),{schema:i,setSchemaReference:o}=nd(),s=Zu();let a,l;r[0]!==n||r[1]!==t||r[2]!==s||r[3]!==i||r[4]!==o?(a=()=>{if(!t)return;const e=(e,t)=>{!function(e,t,{schema:n,setSchemaReference:r},i,o){function s(e){const t=null==i?void 0:i.referencePlugin;if(!(n&&t&&e.currentTarget instanceof HTMLElement))return;const s=e.currentTarget.textContent||"",a=n.getType(s);a&&(i.setVisiblePlugin(t),r({kind:"Type",type:a}),null==o||o(a))}qu([],{useCommonAddons:!1}).then((e=>{let n,r,i,o,a,l,c,u,d;e.on(t,"select",((e,t)=>{if(!n){const e=t.parentNode;n=document.createElement("div"),n.className="CodeMirror-hint-information",e.append(n);const f=document.createElement("header");f.className="CodeMirror-hint-information-header",n.append(f),r=document.createElement("span"),r.className="CodeMirror-hint-information-field-name",f.append(r),i=document.createElement("span"),i.className="CodeMirror-hint-information-type-name-pill",f.append(i),o=document.createElement("span"),i.append(o),a=document.createElement("a"),a.className="CodeMirror-hint-information-type-name",a.href="/service/javascript:void 0",a.addEventListener("click",s),i.append(a),l=document.createElement("span"),i.append(l),c=document.createElement("div"),c.className="CodeMirror-hint-information-description",n.append(c),u=document.createElement("div"),u.className="CodeMirror-hint-information-deprecation",n.append(u);const p=document.createElement("span");p.className="CodeMirror-hint-information-deprecation-label",p.textContent="Deprecated",u.append(p),d=document.createElement("div"),d.className="CodeMirror-hint-information-deprecation-reason",u.append(d);const h=parseInt(window.getComputedStyle(n).paddingBottom.replace(/px$/,""),10)||0,m=parseInt(window.getComputedStyle(n).maxHeight.replace(/px$/,""),10)||0,g=()=>{n&&(n.style.paddingTop=e.scrollTop+h+"px",n.style.maxHeight=e.scrollTop+m+"px")};let v;e.addEventListener("scroll",g),e.addEventListener("DOMNodeRemoved",v=t=>{t.target===e&&(e.removeEventListener("scroll",g),e.removeEventListener("DOMNodeRemoved",v),null==n||n.removeEventListener("click",s),n=null,r=null,i=null,o=null,a=null,l=null,c=null,u=null,d=null,v=null)})}if(r&&(r.textContent=e.text),i&&o&&a&&l)if(e.type){i.style.display="inline";const t=e=>{At(e)?(l.textContent="!"+l.textContent,t(e.ofType)):Dt(e)?(o.textContent+="[",l.textContent="]"+l.textContent,t(e.ofType)):a.textContent=e.name};o.textContent="",l.textContent="",t(e.type)}else o.textContent="",a.textContent="",l.textContent="",i.style.display="none";c&&(e.description?(c.style.display="block",c.innerHTML=Jp.render(e.description)):(c.style.display="none",c.innerHTML="")),u&&d&&(e.deprecationReason?(u.style.display="block",d.innerHTML=Jp.render(e.deprecationReason)):(u.style.display="none",d.innerHTML=""))}))}))}(0,t,{schema:i,setSchemaReference:o},s,(e=>{null==n||n({kind:"Type",type:e,schema:i||void 0})}))};return t.on("hasCompletion",e),()=>t.off("hasCompletion",e)},l=[n,t,s,i,o],r[0]=n,r[1]=t,r[2]=s,r[3]=i,r[4]=o,r[5]=a,r[6]=l):(a=r[5],l=r[6]),e.useEffect(a,l)}function ih(t,n,r){const i=h.c(5);let o,s;i[0]!==r||i[1]!==t||i[2]!==n?(o=()=>{if(t){for(const e of n)t.removeKeyMap(e);if(r){const e={};for(const t of n)e[t]=()=>r();t.addKeyMap(e)}}},s=[t,n,r],i[0]=r,i[1]=t,i[2]=n,i[3]=o,i[4]=s):(o=i[3],s=i[4]),e.useEffect(o,s)}const oh=ch,sh=uh,ah=fh,lh=ph;function ch(e){const t=h.c(7);let n;t[0]!==e?(n=void 0===e?{}:e,t[0]=e,t[1]=n):n=t[1];const{caller:r,onCopyQuery:i}=n,o=r||oh;let s;t[2]!==o?(s={nonNull:!0,caller:o},t[2]=o,t[3]=s):s=t[3];const{queryEditor:a}=Qh(s);let l;return t[4]!==i||t[5]!==a?(l=()=>{if(!a)return;const e=a.getValue();Qu(e),null==i||i(e)},t[4]=i,t[5]=a,t[6]=l):l=t[6],l}function uh(e){const t=h.c(7);let n;t[0]!==e?(n=void 0===e?{}:e,t[0]=e,t[1]=n):n=t[1];const{caller:r}=n,i=r||sh;let o;t[2]!==i?(o={nonNull:!0,caller:i},t[2]=i,t[3]=o):o=t[3];const{queryEditor:s}=Qh(o),{schema:a}=nd();let l;return t[4]!==s||t[5]!==a?(l=()=>{const e=null==s?void 0:s.documentAST,t=null==s?void 0:s.getValue();e&&t&&s.setValue(ut(Rs(e,a)))},t[4]=s,t[5]=a,t[6]=l):l=t[6],l}function dh(e){return ut(je(e))}function fh(e){const t=h.c(9);let n;t[0]!==e?(n=void 0===e?{}:e,t[0]=e,t[1]=n):n=t[1];const{caller:r,onPrettifyQuery:i}=n,o=void 0===i?dh:i,s=r||ah;let a;t[2]!==s?(a={nonNull:!0,caller:s},t[2]=s,t[3]=a):a=t[3];const{queryEditor:l,headerEditor:c,variableEditor:u}=Qh(a);let d;return t[4]!==c||t[5]!==o||t[6]!==l||t[7]!==u?(d=async()=>{if(u){const e=u.getValue();try{const t=JSON.stringify(JSON.parse(e),null,2);t!==e&&u.setValue(t)}catch{}}if(c){const e=c.getValue();try{const t=JSON.stringify(JSON.parse(e),null,2);t!==e&&c.setValue(t)}catch{}}if(l){const e=l.getValue();try{const t=await o(e);t!==e&&l.setValue(t)}catch{}}},t[4]=c,t[5]=o,t[6]=l,t[7]=u,t[8]=d):d=t[8],d}function ph(e){const t=h.c(8);let n;t[0]!==e?(n=void 0===e?{}:e,t[0]=e,t[1]=n):n=t[1];const{getDefaultFieldNames:r,caller:i}=n,{schema:o}=nd(),s=i||lh;let a;t[2]!==s?(a={nonNull:!0,caller:s},t[2]=s,t[3]=a):a=t[3];const{queryEditor:l}=Qh(a);let c;return t[4]!==r||t[5]!==l||t[6]!==o?(c=()=>{if(!l)return;const e=l.getValue(),{insertions:t,result:n}=As(o,e,r);return t&&t.length>0&&l.operation((()=>{const e=l.getCursor(),r=l.indexFromPos(e);let i;l.setValue(n||""),i=0;const o=t.map((e=>{const{index:t,string:n}=e;return i+=n.length,l.markText(l.posFromIndex(t+i),l.posFromIndex(t+i),{className:"auto-inserted-leaf",clearOnEnter:!0,title:"Automatically added leaf fields"})}));setTimeout((()=>{for(const e of o)e.clear()}),7e3);let s=r;for(const{index:n,string:a}of t)n{const n=Qh({nonNull:!0})[`${t}Editor`];let r="";const i=(null==n?void 0:n.getValue())??!1;i&&i.length>0&&(r=i);const o=e.useCallback((e=>null==n?void 0:n.setValue(e)),[n]);return e.useMemo((()=>[r,o]),[r,o])};const mh=gh;function gh(t,n){const r=h.c(17);let i;r[0]!==t?(i=void 0===t?{}:t,r[0]=t,r[1]=i):i=r[1];const{editorTheme:o,keyMap:s,onEdit:a,readOnly:l}=i,c=void 0===o?$u:o,u=void 0===s?Uu:s,d=void 0!==l&&l,f=n||mh;let p;r[2]!==f?(p={nonNull:!0,caller:f},r[2]=f,r[3]=p):p=r[3];const{initialHeaders:m,headerEditor:g,setHeaderEditor:v,shouldPersistHeaders:y}=Qh(p),b=em(),E=n||mh;let x;r[4]!==E?(x={caller:E},r[4]=E,r[5]=x):x=r[5];const w=uh(x),T=n||mh;let C;r[6]!==T?(C={caller:T},r[6]=T,r[7]=C):C=r[7];const S=fh(C),k=e.useRef(null);let _,N,D,A,I;return r[8]!==c||r[9]!==m||r[10]!==d||r[11]!==v?(_=()=>{let e;return e=!0,qu([Promise.resolve().then((()=>UV))]).then((t=>{if(!e)return;const n=k.current;if(!n)return;const r=t(n,{value:m,lineNumbers:!0,tabSize:2,mode:{name:"javascript",json:!0},theme:c,autoCloseBrackets:!0,matchBrackets:!0,showCursorWhenSelecting:!0,readOnly:!!d&&"nocursor",foldGutter:!0,gutters:["CodeMirror-linenumbers","CodeMirror-foldgutter"],extraKeys:Hu});r.addKeyMap({"Cmd-Space"(){r.showHint({completeSingle:!1,container:n})},"Ctrl-Space"(){r.showHint({completeSingle:!1,container:n})},"Alt-Space"(){r.showHint({completeSingle:!1,container:n})},"Shift-Space"(){r.showHint({completeSingle:!1,container:n})}}),r.on("keyup",vh),v(r)})),()=>{e=!1}},N=[c,m,d,v],r[8]=c,r[9]=m,r[10]=d,r[11]=v,r[12]=_,r[13]=N):(_=r[12],N=r[13]),e.useEffect(_,N),th(g,"keyMap",u),nh(g,a,y?yh:null,"headers",mh),r[14]===Symbol.for("react.memo_cache_sentinel")?(D=["Cmd-Enter","Ctrl-Enter"],r[14]=D):D=r[14],ih(g,D,null==b?void 0:b.run),r[15]===Symbol.for("react.memo_cache_sentinel")?(A=["Shift-Ctrl-P"],r[15]=A):A=r[15],ih(g,A,S),r[16]===Symbol.for("react.memo_cache_sentinel")?(I=["Shift-Ctrl-M"],r[16]=I):I=r[16],ih(g,I,w),k}function vh(e,t){const{code:n,key:r,shiftKey:i}=t,o=n.startsWith("Key"),s=!i&&n.startsWith("Digit");(o||s||"_"===r||'"'===r)&&e.execCommand("autocomplete")}const yh="headers",bh=Array.from({length:11},((e,t)=>String.fromCharCode(8192+t))).concat(["\u2028","\u2029"," "," "]),Eh=new RegExp("["+bh.join("")+"]","g");function xh(e){return e.replace(Eh," ")}const wh=Th;function Th(t,n){const r=h.c(40);let i;r[0]!==t?(i=void 0===t?{}:t,r[0]=t,r[1]=i):i=r[1];const{editorTheme:o,keyMap:s,onClickReference:a,onCopyQuery:l,onEdit:c,onPrettifyQuery:u,readOnly:d}=i,f=void 0===o?$u:o,p=void 0===s?Uu:s,m=void 0!==d&&d,{schema:g,setSchemaReference:v}=nd(),y=n||wh;let b;r[2]!==y?(b={nonNull:!0,caller:y},r[2]=y,r[3]=b):b=r[3];const{externalFragments:E,initialQuery:x,queryEditor:w,setOperationName:T,setQueryEditor:C,validationRules:S,variableEditor:k,updateActiveTabValues:_}=Qh(b),N=em(),D=Pu(),A=Zu(),I=n||wh;let O;r[4]!==l||r[5]!==I?(O={caller:I,onCopyQuery:l},r[4]=l,r[5]=I,r[6]=O):O=r[6];const L=ch(O),M=n||wh;let R;r[7]!==M?(R={caller:M},r[7]=M,r[8]=R):R=r[8];const F=uh(R),P=n||wh;let j;r[9]!==u||r[10]!==P?(j={caller:P,onPrettifyQuery:u},r[9]=u,r[10]=P,r[11]=j):j=r[11];const V=fh(j),B=e.useRef(null),$=e.useRef(void 0),U=e.useRef(_h);let H,q,W,z,G,K;r[12]!==a||r[13]!==A||r[14]!==v?(H=()=>{U.current=e=>{const t=null==A?void 0:A.referencePlugin;t&&(A.setVisiblePlugin(t),v(e),null==a||a(e))}},q=[a,A,v],r[12]=a,r[13]=A,r[14]=v,r[15]=H,r[16]=q):(H=r[15],q=r[16]),e.useEffect(H,q),r[17]!==f||r[18]!==x||r[19]!==m||r[20]!==C?(W=()=>{let e;return e=!0,qu([Promise.resolve().then((()=>zV)),Promise.resolve().then((()=>QV)),Promise.resolve().then((()=>XV)),Promise.resolve().then((()=>eB)),Promise.resolve().then((()=>gB)),Promise.resolve().then((()=>TB)),Promise.resolve().then((()=>SB))]).then((t=>{if(!e)return;$.current=t;const n=B.current;if(!n)return;const r=t(n,{value:x,lineNumbers:!0,tabSize:2,foldGutter:!0,mode:"graphql",theme:f,autoCloseBrackets:!0,matchBrackets:!0,showCursorWhenSelecting:!0,readOnly:!!m&&"nocursor",lint:{schema:void 0,validationRules:null,externalFragments:void 0},hintOptions:{schema:void 0,closeOnUnfocus:!1,completeSingle:!1,container:n,externalFragments:void 0,autocompleteOptions:{mode:jc.EXECUTABLE}},info:{schema:void 0,renderDescription:kh,onClick(e){U.current(e)}},jump:{schema:void 0,onClick(e){U.current(e)}},gutters:["CodeMirror-linenumbers","CodeMirror-foldgutter"],extraKeys:{...Hu,"Cmd-S"(){},"Ctrl-S"(){}}}),i=function(){r.showHint({completeSingle:!0,container:n})};let o;r.addKeyMap({"Cmd-Space":i,"Ctrl-Space":i,"Alt-Space":i,"Shift-Space":i,"Shift-Alt-Space":i}),r.on("keyup",Sh),o=!1,r.on("startCompletion",(()=>{o=!0})),r.on("endCompletion",(()=>{o=!1})),r.on("keydown",((e,t)=>{"Escape"===t.key&&o&&t.stopPropagation()})),r.on("beforeChange",Ch),r.documentAST=null,r.operationName=null,r.operations=null,r.variableToType=null,C(r)})),()=>{e=!1}},z=[f,x,m,C],r[17]=f,r[18]=x,r[19]=m,r[20]=C,r[21]=W,r[22]=z):(W=r[21],z=r[22]),e.useEffect(W,z),th(w,"keyMap",p),r[23]!==c||r[24]!==w||r[25]!==g||r[26]!==T||r[27]!==D||r[28]!==_||r[29]!==k?(G=()=>{if(!w)return;const e=function(e){var t;const n=function(e,n){if(n)try{const t=je(n);return Object.assign(Object.assign({},ou(t,e)),{documentAST:t})}catch(t){return}}(g,e.getValue()),r=function(e,t,n){if(!n||n.length<1)return;const r=n.map((e=>{var t;return null==(t=e.name)?void 0:t.value}));if(t&&r.includes(t))return t;if(t&&e){const n=e.map((e=>{var t;return null==(t=e.name)?void 0:t.value})).indexOf(t);if(-1!==n&&n{const n=t.getValue();D.set(Dh,n);const r=t.operationName,i=e(t);void 0!==(null==i?void 0:i.operationName)&&D.set(Ah,i.operationName),null==c||c(n,null==i?void 0:i.documentAST),(null==i?void 0:i.operationName)&&r!==i.operationName&&T(i.operationName),_({query:n,operationName:(null==i?void 0:i.operationName)??null})}));return e(w),w.on("change",t),()=>w.off("change",t)},K=[c,w,g,T,D,k,_],r[23]=c,r[24]=w,r[25]=g,r[26]=T,r[27]=D,r[28]=_,r[29]=k,r[30]=G,r[31]=K):(G=r[30],K=r[31]),e.useEffect(G,K),function(t,n,r){const i=h.c(5);let o,s;i[0]!==r||i[1]!==t||i[2]!==n?(o=()=>{if(!t)return;const e=t.options.lint.schema!==n;!function(e,t){e.state.lint.linterOptions.schema=t,e.options.lint.schema=t,e.options.hintOptions.schema=t,e.options.info.schema=t,e.options.jump.schema=t}(t,n),e&&r.current&&r.current.signal(t,"change",t)},s=[t,n,r],i[0]=r,i[1]=t,i[2]=n,i[3]=o,i[4]=s):(o=i[3],s=i[4]);e.useEffect(o,s)}(w,g??null,$),function(t,n,r){const i=h.c(5);let o,s;i[0]!==r||i[1]!==t||i[2]!==n?(o=()=>{if(!t)return;const e=t.options.lint.validationRules!==n;!function(e,t){e.state.lint.linterOptions.validationRules=t,e.options.lint.validationRules=t}(t,n),e&&r.current&&r.current.signal(t,"change",t)},s=[t,n,r],i[0]=r,i[1]=t,i[2]=n,i[3]=o,i[4]=s):(o=i[3],s=i[4]);e.useEffect(o,s)}(w,S??null,$),function(t,n,r){const i=h.c(7);let o;i[0]!==n?(o=[...n.values()],i[0]=n,i[1]=o):o=i[1];const s=o;let a,l;i[2]!==r||i[3]!==t||i[4]!==s?(a=()=>{if(!t)return;const e=t.options.lint.externalFragments!==s;!function(e,t){e.state.lint.linterOptions.externalFragments=t,e.options.lint.externalFragments=t,e.options.hintOptions.externalFragments=t}(t,s),e&&r.current&&r.current.signal(t,"change",t)},l=[t,s,r],i[2]=r,i[3]=t,i[4]=s,i[5]=a,i[6]=l):(a=i[5],l=i[6]);e.useEffect(a,l)}(w,E,$),rh(w,a);const Y=null==N?void 0:N.run;let Q;r[32]!==w||r[33]!==Y||r[34]!==T?(Q=()=>{var e;if(!(Y&&w&&w.operations&&w.hasFocus()))return void(null==Y||Y());const t=w.indexFromPos(w.getCursor());let n;for(const r of w.operations)r.loc&&r.loc.start<=t&&r.loc.end>=t&&(n=null==(e=r.name)?void 0:e.value);n&&n!==w.operationName&&T(n),Y()},r[32]=w,r[33]=Y,r[34]=T,r[35]=Q):Q=r[35];const X=Q;let J,Z,ee,te;return r[36]===Symbol.for("react.memo_cache_sentinel")?(J=["Cmd-Enter","Ctrl-Enter"],r[36]=J):J=r[36],ih(w,J,X),r[37]===Symbol.for("react.memo_cache_sentinel")?(Z=["Shift-Ctrl-C"],r[37]=Z):Z=r[37],ih(w,Z,L),r[38]===Symbol.for("react.memo_cache_sentinel")?(ee=["Shift-Ctrl-P","Shift-Ctrl-F"],r[38]=ee):ee=r[38],ih(w,ee,V),r[39]===Symbol.for("react.memo_cache_sentinel")?(te=["Shift-Ctrl-M"],r[39]=te):te=r[39],ih(w,te,F),B}function Ch(e,t){var n;if("paste"===t.origin){const e=t.text.map(xh);null==(n=t.update)||n.call(t,t.from,t.to,e)}}function Sh(e,t){Nh.test(t.key)&&e.execCommand("autocomplete")}function kh(e){return Jp.render(e)}function _h(){}const Nh=/^[a-zA-Z0-9_@(]$/,Dh="query",Ah="operationName";function Ih({defaultQuery:e,defaultHeaders:t,headers:n,defaultTabs:r,query:i,variables:o,shouldPersistHeaders:s}){const{storage:a}=Mu.getState(),l=a.get(Uh);try{if(!l)throw new Error("Storage for tabs is empty");const e=JSON.parse(l),t=s?n:void 0;if((c=e)&&"object"==typeof c&&!Array.isArray(c)&&function(e,t){return t in e&&"number"==typeof e[t]}(c,"activeTabIndex")&&"tabs"in c&&Array.isArray(c.tabs)&&c.tabs.every(Oh)){const r=Vh({query:i,variables:o,headers:t});let s=-1;for(let t=0;t=0)e.activeTabIndex=s;else{const t=i?Bh(i):null;e.tabs.push({id:jh(),hash:r,title:t||$h,query:i,variables:o,headers:n,operationName:t,response:null}),e.activeTabIndex=e.tabs.length-1}return e}throw new Error("Storage for tabs is invalid")}catch{return{activeTabIndex:0,tabs:(r||[{query:i??e,variables:o,headers:n??t}]).map(Fh)}}var c}function Oh(e){return e&&"object"==typeof e&&!Array.isArray(e)&&Lh(e,"id")&&Lh(e,"title")&&Mh(e,"query")&&Mh(e,"variables")&&Mh(e,"headers")&&Mh(e,"operationName")&&Mh(e,"response")}function Lh(e,t){return t in e&&"string"==typeof e[t]}function Mh(e,t){return t in e&&("string"==typeof e[t]||null===e[t])}function Rh(e,t=!1){return JSON.stringify(e,((e,n)=>"hash"===e||"response"===e||!t&&"headers"===e?null:n))}function Fh({query:e=null,variables:t=null,headers:n=null}={}){const r=e?Bh(e):null;return{id:jh(),hash:Vh({query:e,variables:t,headers:n}),title:r||$h,query:e,variables:t,headers:n,operationName:r,response:null}}function Ph(e,t){return{...e,tabs:e.tabs.map(((n,r)=>{if(r!==e.activeTabIndex)return n;const i={...n,...t};return{...i,hash:Vh(i),title:i.operationName||(i.query?Bh(i.query):void 0)||$h}}))}}function jh(){const e=()=>Math.floor(65536*(1+Math.random())).toString(16).slice(1);return`${e()}${e()}-${e()}-${e()}-${e()}-${e()}${e()}${e()}`}function Vh(e){return[e.query??"",e.variables??"",e.headers??""].join("|")}function Bh(e){const t=/^(?!#).*(query|subscription|mutation)\s+([a-zA-Z0-9_]+)/m.exec(e);return(null==t?void 0:t[2])??null}const $h="",Uh="tabState";const Hh=qh;function qh(t,n){const r=h.c(17);let i;r[0]!==t?(i=void 0===t?{}:t,r[0]=t,r[1]=i):i=r[1];const{editorTheme:o,keyMap:s,onClickReference:a,onEdit:l,readOnly:c}=i,u=void 0===o?$u:o,d=void 0===s?Uu:s,f=void 0!==c&&c,p=n||Hh;let m;r[2]!==p?(m={nonNull:!0,caller:p},r[2]=p,r[3]=m):m=r[3];const{initialVariables:g,variableEditor:v,setVariableEditor:y}=Qh(m),b=em(),E=n||Hh;let x;r[4]!==E?(x={caller:E},r[4]=E,r[5]=x):x=r[5];const w=uh(x),T=n||Hh;let C;r[6]!==T?(C={caller:T},r[6]=T,r[7]=C):C=r[7];const S=fh(C),k=e.useRef(null);let _,N,D,A,I;return r[8]!==u||r[9]!==g||r[10]!==f||r[11]!==y?(_=()=>{let e;return e=!0,qu([Promise.resolve().then((()=>AB)),Promise.resolve().then((()=>ZB)),Promise.resolve().then((()=>r$))]).then((t=>{if(!e)return;const n=k.current;if(!n)return;const r=t(n,{value:g,lineNumbers:!0,tabSize:2,mode:"graphql-variables",theme:u,autoCloseBrackets:!0,matchBrackets:!0,showCursorWhenSelecting:!0,readOnly:!!f&&"nocursor",foldGutter:!0,lint:{variableToType:void 0},hintOptions:{closeOnUnfocus:!1,completeSingle:!1,container:n,variableToType:void 0},gutters:["CodeMirror-linenumbers","CodeMirror-foldgutter"],extraKeys:Hu});r.addKeyMap({"Cmd-Space"(){r.showHint({completeSingle:!1,container:n})},"Ctrl-Space"(){r.showHint({completeSingle:!1,container:n})},"Alt-Space"(){r.showHint({completeSingle:!1,container:n})},"Shift-Space"(){r.showHint({completeSingle:!1,container:n})}}),r.on("keyup",Wh),y(r)})),()=>{e=!1}},N=[u,g,f,y],r[8]=u,r[9]=g,r[10]=f,r[11]=y,r[12]=_,r[13]=N):(_=r[12],N=r[13]),e.useEffect(_,N),th(v,"keyMap",d),nh(v,l,zh,"variables",Hh),rh(v,a),r[14]===Symbol.for("react.memo_cache_sentinel")?(D=["Cmd-Enter","Ctrl-Enter"],r[14]=D):D=r[14],ih(v,D,null==b?void 0:b.run),r[15]===Symbol.for("react.memo_cache_sentinel")?(A=["Shift-Ctrl-P"],r[15]=A):A=r[15],ih(v,A,S),r[16]===Symbol.for("react.memo_cache_sentinel")?(I=["Shift-Ctrl-M"],r[16]=I):I=r[16],ih(v,I,w),k}function Wh(e,t){const{code:n,key:r,shiftKey:i}=t,o=n.startsWith("Key"),s=!i&&n.startsWith("Digit");(o||s||"_"===r||'"'===r)&&e.execCommand("autocomplete")}const zh="variables",Gh='# Welcome to GraphiQL\n#\n# GraphiQL is an in-browser tool for writing, validating, and\n# testing GraphQL queries.\n#\n# Type queries into this side of the screen, and you will see intelligent\n# typeaheads aware of the current GraphQL type schema and live syntax and\n# validation errors highlighted within the text.\n#\n# GraphQL queries typically start with a "{" character. Lines that start\n# with a # are ignored.\n#\n# An example GraphQL query might look like:\n#\n# {\n# field(arg: "value") {\n# subField\n# }\n# }\n#\n# Keyboard shortcuts:\n#\n# Prettify query: Shift-Ctrl-P (or press the prettify button)\n#\n# Merge fragments: Shift-Ctrl-M (or press the merge button)\n#\n# Run Query: Ctrl-Enter (or press the play button)\n#\n# Auto Complete: Ctrl-Space (or just start typing)\n#\n\n',Kh=Nu("EditorContext"),Yh=t=>{const n=h.c(88),r=Pu(),[i,o]=e.useState(null),[s,a]=e.useState(null),[l,c]=e.useState(null),[u,d]=e.useState(null);let f;n[0]!==t.shouldPersistHeaders||n[1]!==r?(f=()=>{const e=null!==r.get(Xh);return!1!==t.shouldPersistHeaders&&e?"true"===r.get(Xh):Boolean(t.shouldPersistHeaders)},n[0]=t.shouldPersistHeaders,n[1]=r,n[2]=f):f=n[2];const[m,g]=e.useState(f);let v;eh(i,t.headers),eh(s,t.query),eh(l,t.response),eh(u,t.variables),n[3]!==m?(v={shouldPersistHeaders:m},n[3]=m,n[4]=v):v=n[4];const y=function({shouldPersistHeaders:t}){return e.useCallback((e=>{const{storage:n}=Mu.getState();Zp(500,(e=>{n.set(Uh,e)}))(Rh(e,t))}),[t])}(v);let b;n[5]!==t.defaultHeaders||n[6]!==t.defaultQuery||n[7]!==t.defaultTabs||n[8]!==t.headers||n[9]!==t.query||n[10]!==t.response||n[11]!==t.variables||n[12]!==m||n[13]!==r||n[14]!==y?(b=()=>{const e=t.query??r.get(Dh)??null,n=t.variables??r.get(zh)??null,i=t.headers??r.get(yh)??null,o=t.response??"",s=Ih({query:e,variables:n,headers:i,defaultTabs:t.defaultTabs,defaultQuery:t.defaultQuery||Gh,defaultHeaders:t.defaultHeaders,shouldPersistHeaders:m});return y(s),{query:e??(0===s.activeTabIndex?s.tabs[0].query:null)??"",variables:n??"",headers:i??t.defaultHeaders??"",response:o,tabState:s}},n[5]=t.defaultHeaders,n[6]=t.defaultQuery,n[7]=t.defaultTabs,n[8]=t.headers,n[9]=t.query,n[10]=t.response,n[11]=t.variables,n[12]=m,n[13]=r,n[14]=y,n[15]=b):b=n[15];const[E]=e.useState(b),[x,w]=e.useState(E.tabState);let T;n[16]!==i||n[17]!==r||n[18]!==x?(T=e=>{if(e){r.set(yh,(null==i?void 0:i.getValue())??"");const e=Rh(x,!0);r.set(Uh,e)}else r.set(yh,""),function(){const{storage:e}=Mu.getState(),t=e.get(Uh);if(t){const n=JSON.parse(t);e.set(Uh,JSON.stringify(n,((e,t)=>"headers"===e?null:t)))}}();g(e),r.set(Xh,e.toString())},n[16]=i,n[17]=r,n[18]=x,n[19]=T):T=n[19];const C=T,S=e.useRef(void 0);let k,_,N;n[20]!==t.shouldPersistHeaders||n[21]!==C?(k=()=>{const e=Boolean(t.shouldPersistHeaders);(null==S?void 0:S.current)!==e&&(C(e),S.current=e)},_=[t.shouldPersistHeaders,C],n[20]=t.shouldPersistHeaders,n[21]=C,n[22]=k,n[23]=_):(k=n[22],_=n[23]),e.useEffect(k,_),n[24]!==i||n[25]!==s||n[26]!==l||n[27]!==u?(N={queryEditor:s,variableEditor:u,headerEditor:i,responseEditor:l},n[24]=i,n[25]=s,n[26]=l,n[27]=u,n[28]=N):N=n[28];const D=function({queryEditor:t,variableEditor:n,headerEditor:r,responseEditor:i}){return e.useCallback((e=>{const o=(null==t?void 0:t.getValue())??null,s=(null==n?void 0:n.getValue())??null,a=(null==r?void 0:r.getValue())??null,l=(null==t?void 0:t.operationName)??null;return Ph(e,{query:o,variables:s,headers:a,response:(null==i?void 0:i.getValue())??null,operationName:l})}),[t,n,r,i])}(N),{onTabChange:A,defaultHeaders:I,defaultQuery:O,children:L}=t;let M;n[29]!==I||n[30]!==i||n[31]!==s||n[32]!==l||n[33]!==u?(M={queryEditor:s,variableEditor:u,headerEditor:i,responseEditor:l,defaultHeaders:I},n[29]=I,n[30]=i,n[31]=s,n[32]=l,n[33]=u,n[34]=M):M=n[34];const R=function({queryEditor:t,variableEditor:n,headerEditor:r,responseEditor:i,defaultHeaders:o}){return e.useCallback((({query:e,variables:s,headers:a,response:l})=>{null==t||t.setValue(e??""),null==n||n.setValue(s??""),null==r||r.setValue(a??o??""),null==i||i.setValue(l??"")}),[r,t,i,n,o])}(M);let F;n[35]!==I||n[36]!==O||n[37]!==A||n[38]!==R||n[39]!==y||n[40]!==D?(F=()=>{w((e=>{const t=D(e),n={tabs:[...t.tabs,Fh({headers:I,query:O??Gh})],activeTabIndex:t.tabs.length};return y(n),R(n.tabs[n.activeTabIndex]),null==A||A(n),n}))},n[35]=I,n[36]=O,n[37]=A,n[38]=R,n[39]=y,n[40]=D,n[41]=F):F=n[41];const P=F;let j;n[42]!==A||n[43]!==R||n[44]!==y?(j=e=>{w((t=>{const n={...t,activeTabIndex:e};return y(n),R(n.tabs[n.activeTabIndex]),null==A||A(n),n}))},n[42]=A,n[43]=R,n[44]=y,n[45]=j):j=n[45];const V=j;let B;n[46]!==A||n[47]!==R||n[48]!==y?(B=e=>{w((t=>{const n=t.tabs[t.activeTabIndex],r={tabs:e,activeTabIndex:e.indexOf(n)};return y(r),R(r.tabs[r.activeTabIndex]),null==A||A(r),r}))},n[46]=A,n[47]=R,n[48]=y,n[49]=B):B=n[49];const $=B;let U;n[50]!==A||n[51]!==R||n[52]!==y?(U=e=>{w((t=>{const n={tabs:t.tabs.filter(((t,n)=>e!==n)),activeTabIndex:Math.max(t.activeTabIndex-1,0)};return y(n),R(n.tabs[n.activeTabIndex]),null==A||A(n),n}))},n[50]=A,n[51]=R,n[52]=y,n[53]=U):U=n[53];const H=U;let q;n[54]!==A||n[55]!==y?(q=e=>{w((t=>{const n=Ph(t,e);return y(n),null==A||A(n),n}))},n[54]=A,n[55]=y,n[56]=q):q=n[56];const W=q,{onEditOperationName:z}=t;let G;n[57]!==z||n[58]!==s||n[59]!==W?(G=e=>{s&&(!function(e,t){e.operationName=t}(s,e),W({operationName:e}),null==z||z(e))},n[57]=z,n[58]=s,n[59]=W,n[60]=G):G=n[60];const K=G;let Y,Q;if(n[61]!==t.externalFragments){if(Q=new Map,Array.isArray(t.externalFragments))for(const e of t.externalFragments)Q.set(e.name.value,e);else if("string"==typeof t.externalFragments)at(je(t.externalFragments,{}),{FragmentDefinition(e){Q.set(e.name.value,e)}});else if(t.externalFragments)throw new Error("The `externalFragments` prop must either be a string that contains the fragment definitions in SDL or a list of FragmentDefinitionNode objects.");n[61]=t.externalFragments,n[62]=Q}else Q=n[62];Y=Q;const X=Y;let J;n[63]!==t.validationRules?(J=t.validationRules||[],n[63]=t.validationRules,n[64]=J):J=n[64];const Z=J;let ee;n[65]!==P||n[66]!==V||n[67]!==H||n[68]!==X||n[69]!==i||n[70]!==E.headers||n[71]!==E.query||n[72]!==E.response||n[73]!==E.variables||n[74]!==$||n[75]!==s||n[76]!==l||n[77]!==K||n[78]!==C||n[79]!==m||n[80]!==x||n[81]!==W||n[82]!==Z||n[83]!==u?(ee={...x,addTab:P,changeTab:V,moveTab:$,closeTab:H,updateActiveTabValues:W,headerEditor:i,queryEditor:s,responseEditor:l,variableEditor:u,setHeaderEditor:o,setQueryEditor:a,setResponseEditor:c,setVariableEditor:d,setOperationName:K,initialQuery:E.query,initialVariables:E.variables,initialHeaders:E.headers,initialResponse:E.response,externalFragments:X,validationRules:Z,shouldPersistHeaders:m,setShouldPersistHeaders:C},n[65]=P,n[66]=V,n[67]=H,n[68]=X,n[69]=i,n[70]=E.headers,n[71]=E.query,n[72]=E.response,n[73]=E.variables,n[74]=$,n[75]=s,n[76]=l,n[77]=K,n[78]=C,n[79]=m,n[80]=x,n[81]=W,n[82]=Z,n[83]=u,n[84]=ee):ee=n[84];const te=ee;let ne;return n[85]!==L||n[86]!==te?(ne=p.jsx(Kh.Provider,{value:te,children:L}),n[85]=L,n[86]=te,n[87]=ne):ne=n[87],ne};const Qh=Du(Kh),Xh="shouldPersistHeaders",Jh=Nu("ExecutionContext"),Zh=t=>{const n=h.c(26),{fetcher:r,getDefaultFieldNames:i,children:o,operationName:s}=t;if("function"!=typeof r)throw new TypeError("The `ExecutionContextProvider` component requires a `fetcher` function to be passed as prop.");let a;n[0]===Symbol.for("react.memo_cache_sentinel")?(a={nonNull:!0,caller:Zh},n[0]=a):a=n[0];const{externalFragments:l,headerEditor:c,queryEditor:u,responseEditor:d,variableEditor:f,updateActiveTabValues:m}=Qh(a);let g;n[1]!==i?(g={getDefaultFieldNames:i,caller:Zh},n[1]=i,n[2]=g):g=n[2];const v=ph(g),[y,b]=e.useState(!1),[E,x]=e.useState(null),w=e.useRef(0);let T;n[3]!==E?(T=()=>{null==E||E.unsubscribe(),b(!1),x(null)},n[3]=E,n[4]=T):T=n[4];const C=T;let S;n[5]!==v||n[6]!==l||n[7]!==r||n[8]!==c||n[9]!==s||n[10]!==u||n[11]!==d||n[12]!==C||n[13]!==E||n[14]!==m||n[15]!==f?(S=async()=>{if(!u||!d)return;if(E)return void C();const e=e=>{d.setValue(e),m({response:e})};w.current=w.current+1;const t=w.current;let n=v()||u.getValue();const i=null==f?void 0:f.getValue();let o;try{o=tm({json:i,errorMessageParse:"Variables are invalid JSON",errorMessageType:"Variables are not a JSON object."})}catch(T){const t=T;return void e(t instanceof Error?t.message:`${t}`)}const a=null==c?void 0:c.getValue();let p;try{p=tm({json:a,errorMessageParse:"Headers are invalid JSON",errorMessageType:"Headers are not a JSON object."})}catch(S){const t=S;return void e(t instanceof Error?t.message:`${t}`)}if(l){const e=u.documentAST?((e,t)=>{if(!t)return[];const n=new Map,r=new Set;at(e,{FragmentDefinition(e){n.set(e.name.value,!0)},FragmentSpread(e){r.has(e.name.value)||r.add(e.name.value)}});const i=new Set;for(const s of r)!n.has(s)&&t.has(s)&&i.add(nu(t.get(s)));const o=[];for(const s of i)at(s,{FragmentSpread(e){!r.has(e.name.value)&&t.get(e.name.value)&&(i.add(nu(t.get(e.name.value))),r.add(e.name.value))}}),n.has(s.name.value)||o.push(s);return o})(u.documentAST,l):[];e.length>0&&(n=n+"\n"+e.map(im).join("\n"))}e(""),b(!0);const h=s??u.operationName??void 0,g=p??void 0,y=u.documentAST??void 0;try{const i={},s=n=>{if(t!==w.current)return;let r=!!Array.isArray(n)&&n;if(!r&&"object"==typeof n&&null!==n&&"hasNext"in n&&(r=[n]),r){for(const e of r)rm(i,e);b(!1),e(Ds(i))}else{const t=Ds(n);b(!1),e(t)}},a=r({query:n,variables:o,operationName:h},{headers:g,documentAST:y}),l=await a;_(l)?x(l.subscribe({next(e){s(e)},error(t){b(!1),t&&e(Ns(t)),x(null)},complete(){b(!1),x(null)}})):N(l)?(x({unsubscribe:()=>{var e,t;return null==(t=(e=l[Symbol.asyncIterator]()).return)?void 0:t.call(e)}}),await async function(e,t){for await(const n of t)e(n)}(s,l),b(!1),x(null)):s(l)}catch(k){const t=k;b(!1),e(Ns(t)),x(null)}},n[5]=v,n[6]=l,n[7]=r,n[8]=c,n[9]=s,n[10]=u,n[11]=d,n[12]=C,n[13]=E,n[14]=m,n[15]=f,n[16]=S):S=n[16];const k=S,D=Boolean(E),A=s??null;let I;n[17]!==y||n[18]!==k||n[19]!==C||n[20]!==D||n[21]!==A?(I={isFetching:y,isSubscribed:D,operationName:A,run:k,stop:C},n[17]=y,n[18]=k,n[19]=C,n[20]=D,n[21]=A,n[22]=I):I=n[22];const O=I;let L;return n[23]!==o||n[24]!==O?(L=p.jsx(Jh.Provider,{value:O,children:o}),n[23]=o,n[24]=O,n[25]=L):L=n[25],L};const em=Du(Jh);function tm({json:e,errorMessageParse:t,errorMessageType:n}){let r;try{r=e&&""!==e.trim()?JSON.parse(e):void 0}catch(o){throw new Error(`${t}: ${o instanceof Error?o.message:o}.`)}const i="object"==typeof r&&null!==r&&!Array.isArray(r);if(void 0!==r&&!i)throw new Error(n);return r}const nm=new WeakMap;function rm(e,t){var n,r,i;let o=["data",...t.path??[]];for(const l of[e,t])if(l.pending){let t=nm.get(e);void 0===t&&(t=new Map,nm.set(e,t));for(const{id:e,path:n}of l.pending)t.set(e,["data",...n])}const{items:s}=t;if(s){const{id:r}=t;if(r){if(o=null==(n=nm.get(e))?void 0:n.get(r),void 0===o)throw new Error("Invalid incremental delivery format.");_u(e,o.join(".")).push(...s)}else{o=["data",...t.path??[]];for(const t of s)xu(e,o.join("."),t),o[o.length-1]++}}const{data:a}=t;if(a){const{id:n}=t;if(n){if(o=null==(r=nm.get(e))?void 0:r.get(n),void 0===o)throw new Error("Invalid incremental delivery format.");const{subPath:i}=t;void 0!==i&&(o=[...o,...i])}xu(e,o.join("."),a,{merge:!0})}if(t.errors&&(e.errors||(e.errors=[]),e.errors.push(...t.errors)),t.extensions&&xu(e,"extensions",t.extensions,{merge:!0}),t.incremental)for(const l of t.incremental)rm(e,l);if(t.completed)for(const{id:l,errors:c}of t.completed)null==(i=nm.get(e))||i.delete(l),c&&(e.errors||(e.errors=[]),e.errors.push(...c))}function im(e){return ut(e)}const om=e=>{const t=h.c(45),{children:n,dangerouslyAssumeSchemaIsValid:r,defaultQuery:i,defaultHeaders:o,defaultTabs:s,externalFragments:a,fetcher:l,getDefaultFieldNames:c,headers:u,inputValueDeprecation:d,introspectionQueryName:f,onEditOperationName:m,onSchemaChange:g,onTabChange:v,onTogglePluginVisibility:y,operationName:b,plugins:E,referencePlugin:x,query:w,response:T,schema:C,schemaDescription:S,shouldPersistHeaders:k,storage:_,validationRules:N,variables:D,visiblePlugin:A}=e;let I;t[0]!==o||t[1]!==i||t[2]!==s||t[3]!==a||t[4]!==u||t[5]!==m||t[6]!==v||t[7]!==w||t[8]!==T||t[9]!==k||t[10]!==N||t[11]!==D?(I={defaultQuery:i,defaultHeaders:o,defaultTabs:s,externalFragments:a,headers:u,onEditOperationName:m,onTabChange:v,query:w,response:T,shouldPersistHeaders:k,validationRules:N,variables:D},t[0]=o,t[1]=i,t[2]=s,t[3]=a,t[4]=u,t[5]=m,t[6]=v,t[7]=w,t[8]=T,t[9]=k,t[10]=N,t[11]=D,t[12]=I):I=t[12];const O=I;let L;t[13]!==r||t[14]!==l||t[15]!==d||t[16]!==f||t[17]!==g||t[18]!==C||t[19]!==S?(L={dangerouslyAssumeSchemaIsValid:r,fetcher:l,inputValueDeprecation:d,introspectionQueryName:f,onSchemaChange:g,schema:C,schemaDescription:S},t[13]=r,t[14]=l,t[15]=d,t[16]=f,t[17]=g,t[18]=C,t[19]=S,t[20]=L):L=t[20];const M=L;let R;t[21]!==l||t[22]!==c||t[23]!==b?(R={getDefaultFieldNames:c,fetcher:l,operationName:b},t[21]=l,t[22]=c,t[23]=b,t[24]=R):R=t[24];const F=R;let P;t[25]!==y||t[26]!==E||t[27]!==x||t[28]!==A?(P={onTogglePluginVisibility:y,plugins:E,visiblePlugin:A,referencePlugin:x},t[25]=y,t[26]=E,t[27]=x,t[28]=A,t[29]=P):P=t[29];const j=P;let V,B,$,U,H;return t[30]!==n||t[31]!==j?(V=p.jsx(Ju,{...j,children:n}),t[30]=n,t[31]=j,t[32]=V):V=t[32],t[33]!==F||t[34]!==V?(B=p.jsx(Zh,{...F,children:V}),t[33]=F,t[34]=V,t[35]=B):B=t[35],t[36]!==M||t[37]!==B?($=p.jsx(td,{...M,children:B}),t[36]=M,t[37]=B,t[38]=$):$=t[38],t[39]!==O||t[40]!==$?(U=p.jsx(Yh,{...O,children:$}),t[39]=O,t[40]=$,t[41]=U):U=t[41],t[42]!==_||t[43]!==U?(H=p.jsx(Ru,{storage:_,children:U}),t[42]=_,t[43]=U,t[44]=H):H=t[44],H};function sm(t){const n=h.c(11),r=void 0===t?null:t,i=Pu();let o;n[0]!==r||n[1]!==i?(o=()=>{const e=i.get(am);switch(e){case"light":return"light";case"dark":return"dark";default:return"string"==typeof e&&i.set(am,""),r}},n[0]=r,n[1]=i,n[2]=o):o=n[2];const[s,a]=e.useState(o);let l,c,u;n[3]!==s?(l=()=>{document.body.classList.remove("graphiql-light","graphiql-dark"),s&&document.body.classList.add(`graphiql-${s}`)},c=[s],n[3]=s,n[4]=l,n[5]=c):(l=n[4],c=n[5]),e.useEffect(l,c),n[6]!==i?(u=e=>{i.set(am,e||""),a(e)},n[6]=i,n[7]=u):u=n[7];const d=u;let f;return n[8]!==d||n[9]!==s?(f={theme:s,setTheme:d},n[8]=d,n[9]=s,n[10]=f):f=n[10],f}const am="theme",lm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M5.0484 1.40838C6.12624 0.33054 7.87376 0.330541 8.9516 1.40838L12.5916 5.0484C13.6695 6.12624 13.6695 7.87376 12.5916 8.9516L8.9516 12.5916C7.87376 13.6695 6.12624 13.6695 5.0484 12.5916L1.40838 8.9516C0.33054 7.87376 0.330541 6.12624 1.40838 5.0484L5.0484 1.40838Z",stroke:"currentColor",strokeWidth:1.2}),i.createElement("rect",{x:6,y:6,width:2,height:2,rx:1,fill:"currentColor"})))),cm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 14 9",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M1 1L7 7L13 1",stroke:"currentColor",strokeWidth:1.5})))),um=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 7 10",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M6 1.04819L2 5.04819L6 9.04819",stroke:"currentColor",strokeWidth:1.75})))),dm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 14 9",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M13 8L7 2L1 8",stroke:"currentColor",strokeWidth:1.5})))),fm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 14 14",stroke:"currentColor",strokeWidth:3,xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M1 1L12.9998 12.9997"}),i.createElement("path",{d:"M13 1L1.00079 13.0003"})))),pm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"-2 -2 22 22",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M11.25 14.2105V15.235C11.25 16.3479 10.3479 17.25 9.23501 17.25H2.76499C1.65214 17.25 0.75 16.3479 0.75 15.235L0.75 8.76499C0.75 7.65214 1.65214 6.75 2.76499 6.75L3.78947 6.75",stroke:"currentColor",strokeWidth:1.5}),i.createElement("rect",{x:6.75,y:.75,width:10.5,height:10.5,rx:2.2069,stroke:"currentColor",strokeWidth:1.5})))),hm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M5.0484 1.40838C6.12624 0.33054 7.87376 0.330541 8.9516 1.40838L12.5916 5.0484C13.6695 6.12624 13.6695 7.87376 12.5916 8.9516L8.9516 12.5916C7.87376 13.6695 6.12624 13.6695 5.0484 12.5916L1.40838 8.9516C0.33054 7.87376 0.330541 6.12624 1.40838 5.0484L5.0484 1.40838Z",stroke:"currentColor",strokeWidth:1.2}),i.createElement("path",{d:"M5 9L9 5",stroke:"currentColor",strokeWidth:1.2}),i.createElement("path",{d:"M5 5L9 9",stroke:"currentColor",strokeWidth:1.2})))),mm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 12 12",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M4 8L8 4",stroke:"currentColor",strokeWidth:1.2}),i.createElement("path",{d:"M4 4L8 8",stroke:"currentColor",strokeWidth:1.2}),i.createElement("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M8.5 1.2H9C9.99411 1.2 10.8 2.00589 10.8 3V9C10.8 9.99411 9.99411 10.8 9 10.8H8.5V12H9C10.6569 12 12 10.6569 12 9V3C12 1.34315 10.6569 0 9 0H8.5V1.2ZM3.5 1.2V0H3C1.34315 0 0 1.34315 0 3V9C0 10.6569 1.34315 12 3 12H3.5V10.8H3C2.00589 10.8 1.2 9.99411 1.2 9V3C1.2 2.00589 2.00589 1.2 3 1.2H3.5Z",fill:"currentColor"})))),gm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 12 12",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("rect",{x:.6,y:.6,width:10.8,height:10.8,rx:3.4,stroke:"currentColor",strokeWidth:1.2}),i.createElement("path",{d:"M4 8L8 4",stroke:"currentColor",strokeWidth:1.2}),i.createElement("path",{d:"M4 4L8 8",stroke:"currentColor",strokeWidth:1.2})))),vm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0.5 12 12",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("rect",{x:7,y:5.5,width:2,height:2,rx:1,transform:"rotate(90 7 5.5)",fill:"currentColor"}),i.createElement("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M10.8 9L10.8 9.5C10.8 10.4941 9.99411 11.3 9 11.3L3 11.3C2.00589 11.3 1.2 10.4941 1.2 9.5L1.2 9L-3.71547e-07 9L-3.93402e-07 9.5C-4.65826e-07 11.1569 1.34314 12.5 3 12.5L9 12.5C10.6569 12.5 12 11.1569 12 9.5L12 9L10.8 9ZM10.8 4L12 4L12 3.5C12 1.84315 10.6569 0.5 9 0.5L3 0.5C1.34315 0.5 -5.87117e-08 1.84315 -1.31135e-07 3.5L-1.5299e-07 4L1.2 4L1.2 3.5C1.2 2.50589 2.00589 1.7 3 1.7L9 1.7C9.99411 1.7 10.8 2.50589 10.8 3.5L10.8 4Z",fill:"currentColor"})))),ym=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 20 24",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M0.75 3C0.75 1.75736 1.75736 0.75 3 0.75H17.25C17.8023 0.75 18.25 1.19772 18.25 1.75V5.25",stroke:"currentColor",strokeWidth:1.5}),i.createElement("path",{d:"M0.75 3C0.75 4.24264 1.75736 5.25 3 5.25H18.25C18.8023 5.25 19.25 5.69771 19.25 6.25V22.25C19.25 22.8023 18.8023 23.25 18.25 23.25H3C1.75736 23.25 0.75 22.2426 0.75 21V3Z",stroke:"currentColor",strokeWidth:1.5}),i.createElement("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M3 5.25C1.75736 5.25 0.75 4.24264 0.75 3V21C0.75 22.2426 1.75736 23.25 3 23.25H18.25C18.8023 23.25 19.25 22.8023 19.25 22.25V6.25C19.25 5.69771 18.8023 5.25 18.25 5.25H3ZM13 11L6 11V12.5L13 12.5V11Z",fill:"currentColor"})))),bm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 20 24",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M0.75 3C0.75 4.24264 1.75736 5.25 3 5.25H17.25M0.75 3C0.75 1.75736 1.75736 0.75 3 0.75H16.25C16.8023 0.75 17.25 1.19772 17.25 1.75V5.25M0.75 3V21C0.75 22.2426 1.75736 23.25 3 23.25H18.25C18.8023 23.25 19.25 22.8023 19.25 22.25V6.25C19.25 5.69771 18.8023 5.25 18.25 5.25H17.25",stroke:"currentColor",strokeWidth:1.5}),i.createElement("line",{x1:13,y1:11.75,x2:6,y2:11.75,stroke:"currentColor",strokeWidth:1.5})))),Em=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 12 12",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("rect",{x:5,y:5,width:2,height:2,rx:1,fill:"currentColor"}),i.createElement("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M8.5 1.2H9C9.99411 1.2 10.8 2.00589 10.8 3V9C10.8 9.99411 9.99411 10.8 9 10.8H8.5V12H9C10.6569 12 12 10.6569 12 9V3C12 1.34315 10.6569 0 9 0H8.5V1.2ZM3.5 1.2V0H3C1.34315 0 0 1.34315 0 3V9C0 10.6569 1.34315 12 3 12H3.5V10.8H3C2.00589 10.8 1.2 9.99411 1.2 9V3C1.2 2.00589 2.00589 1.2 3 1.2H3.5Z",fill:"currentColor"})))),xm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 12 13",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("rect",{x:.6,y:1.1,width:10.8,height:10.8,rx:2.4,stroke:"currentColor",strokeWidth:1.2}),i.createElement("rect",{x:5,y:5.5,width:2,height:2,rx:1,fill:"currentColor"})))),wm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 24 20",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M1.59375 9.52344L4.87259 12.9944L8.07872 9.41249",stroke:"currentColor",strokeWidth:1.5,strokeLinecap:"square"}),i.createElement("path",{d:"M13.75 5.25V10.75H18.75",stroke:"currentColor",strokeWidth:1.5,strokeLinecap:"square"}),i.createElement("path",{d:"M4.95427 11.9332C4.55457 10.0629 4.74441 8.11477 5.49765 6.35686C6.25089 4.59894 7.5305 3.11772 9.16034 2.11709C10.7902 1.11647 12.6901 0.645626 14.5986 0.769388C16.5071 0.893151 18.3303 1.60543 19.8172 2.80818C21.3042 4.01093 22.3818 5.64501 22.9017 7.48548C23.4216 9.32595 23.3582 11.2823 22.7203 13.0853C22.0824 14.8883 20.9013 16.4492 19.3396 17.5532C17.778 18.6572 15.9125 19.25 14 19.25",stroke:"currentColor",strokeWidth:1.5})))),Tm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 12 12",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("circle",{cx:6,cy:6,r:5.4,stroke:"currentColor",strokeWidth:1.2,strokeDasharray:"4.241025 4.241025",transform:"rotate(22.5 6 6)"}),i.createElement("circle",{cx:6,cy:6,r:1,fill:"currentColor"})))),Cm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 19 18",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M1.5 14.5653C1.5 15.211 1.75652 15.8303 2.21314 16.2869C2.66975 16.7435 3.28905 17 3.9348 17C4.58054 17 5.19984 16.7435 5.65646 16.2869C6.11307 15.8303 6.36959 15.211 6.36959 14.5653V12.1305H3.9348C3.28905 12.1305 2.66975 12.387 2.21314 12.8437C1.75652 13.3003 1.5 13.9195 1.5 14.5653Z",stroke:"currentColor",strokeWidth:1.125,strokeLinecap:"round",strokeLinejoin:"round"}),i.createElement("path",{d:"M3.9348 1.00063C3.28905 1.00063 2.66975 1.25715 2.21314 1.71375C1.75652 2.17035 1.5 2.78964 1.5 3.43537C1.5 4.0811 1.75652 4.70038 2.21314 5.15698C2.66975 5.61358 3.28905 5.8701 3.9348 5.8701H6.36959V3.43537C6.36959 2.78964 6.11307 2.17035 5.65646 1.71375C5.19984 1.25715 4.58054 1.00063 3.9348 1.00063Z",stroke:"currentColor",strokeWidth:1.125,strokeLinecap:"round",strokeLinejoin:"round"}),i.createElement("path",{d:"M15.0652 12.1305H12.6304V14.5653C12.6304 15.0468 12.7732 15.5175 13.0407 15.9179C13.3083 16.3183 13.6885 16.6304 14.1334 16.8147C14.5783 16.9989 15.0679 17.0472 15.5402 16.9532C16.0125 16.8593 16.4464 16.6274 16.7869 16.2869C17.1274 15.9464 17.3593 15.5126 17.4532 15.0403C17.5472 14.568 17.4989 14.0784 17.3147 13.6335C17.1304 13.1886 16.8183 12.8084 16.4179 12.5409C16.0175 12.2733 15.5468 12.1305 15.0652 12.1305Z",stroke:"currentColor",strokeWidth:1.125,strokeLinecap:"round",strokeLinejoin:"round"}),i.createElement("path",{d:"M12.6318 5.86775H6.36955V12.1285H12.6318V5.86775Z",stroke:"currentColor",strokeWidth:1.125,strokeLinecap:"round",strokeLinejoin:"round"}),i.createElement("path",{d:"M17.5 3.43473C17.5 2.789 17.2435 2.16972 16.7869 1.71312C16.3303 1.25652 15.711 1 15.0652 1C14.4195 1 13.8002 1.25652 13.3435 1.71312C12.8869 2.16972 12.6304 2.789 12.6304 3.43473V5.86946H15.0652C15.711 5.86946 16.3303 5.61295 16.7869 5.15635C17.2435 4.69975 17.5 4.08046 17.5 3.43473Z",stroke:"currentColor",strokeWidth:1.125,strokeLinecap:"round",strokeLinejoin:"round"})))),Sm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 13 13",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("circle",{cx:5,cy:5,r:4.35,stroke:"currentColor",strokeWidth:1.3}),i.createElement("line",{x1:8.45962,y1:8.54038,x2:11.7525,y2:11.8333,stroke:"currentColor",strokeWidth:1.3})))),km=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"-2 -2 22 22",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M17.2492 6V2.9569C17.2492 1.73806 16.2611 0.75 15.0423 0.75L2.9569 0.75C1.73806 0.75 0.75 1.73806 0.75 2.9569L0.75 6",stroke:"currentColor",strokeWidth:1.5}),i.createElement("path",{d:"M0.749873 12V15.0431C0.749873 16.2619 1.73794 17.25 2.95677 17.25H15.0421C16.261 17.25 17.249 16.2619 17.249 15.0431V12",stroke:"currentColor",strokeWidth:1.5}),i.createElement("path",{d:"M6 4.5L9 7.5L12 4.5",stroke:"currentColor",strokeWidth:1.5}),i.createElement("path",{d:"M12 13.5L9 10.5L6 13.5",stroke:"currentColor",strokeWidth:1.5})))),_m=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M0.75 13.25L0.0554307 12.967C-0.0593528 13.2488 0.00743073 13.5719 0.224488 13.7851C0.441545 13.9983 0.765869 14.0592 1.04549 13.9393L0.75 13.25ZM12.8214 1.83253L12.2911 2.36286L12.2911 2.36286L12.8214 1.83253ZM12.8214 3.90194L13.3517 4.43227L12.8214 3.90194ZM10.0981 1.17859L9.56773 0.648259L10.0981 1.17859ZM12.1675 1.17859L12.6978 0.648258L12.6978 0.648257L12.1675 1.17859ZM2.58049 8.75697L3.27506 9.03994L2.58049 8.75697ZM2.70066 8.57599L3.23099 9.10632L2.70066 8.57599ZM5.2479 11.4195L4.95355 10.7297L5.2479 11.4195ZM5.42036 11.303L4.89003 10.7727L5.42036 11.303ZM4.95355 10.7297C4.08882 11.0987 3.41842 11.362 2.73535 11.6308C2.05146 11.9 1.35588 12.1743 0.454511 12.5607L1.04549 13.9393C1.92476 13.5624 2.60256 13.2951 3.28469 13.0266C3.96762 12.7578 4.65585 12.4876 5.54225 12.1093L4.95355 10.7297ZM1.44457 13.533L3.27506 9.03994L1.88592 8.474L0.0554307 12.967L1.44457 13.533ZM3.23099 9.10632L10.6284 1.70892L9.56773 0.648259L2.17033 8.04566L3.23099 9.10632ZM11.6371 1.70892L12.2911 2.36286L13.3517 1.3022L12.6978 0.648258L11.6371 1.70892ZM12.2911 3.37161L4.89003 10.7727L5.95069 11.8333L13.3517 4.43227L12.2911 3.37161ZM12.2911 2.36286C12.5696 2.64142 12.5696 3.09305 12.2911 3.37161L13.3517 4.43227C14.2161 3.56792 14.2161 2.16654 13.3517 1.3022L12.2911 2.36286ZM10.6284 1.70892C10.9069 1.43036 11.3586 1.43036 11.6371 1.70892L12.6978 0.648257C11.8335 -0.216088 10.4321 -0.216084 9.56773 0.648259L10.6284 1.70892ZM3.27506 9.03994C3.26494 9.06479 3.24996 9.08735 3.23099 9.10632L2.17033 8.04566C2.04793 8.16806 1.95123 8.31369 1.88592 8.474L3.27506 9.03994ZM5.54225 12.1093C5.69431 12.0444 5.83339 11.9506 5.95069 11.8333L4.89003 10.7727C4.90863 10.7541 4.92988 10.7398 4.95355 10.7297L5.54225 12.1093Z",fill:"currentColor"}),i.createElement("path",{d:"M11.5 4.5L9.5 2.5",stroke:"currentColor",strokeWidth:1.4026,strokeLinecap:"round",strokeLinejoin:"round"}),i.createElement("path",{d:"M5.5 10.5L3.5 8.5",stroke:"currentColor",strokeWidth:1.4026,strokeLinecap:"round",strokeLinejoin:"round"})))),Nm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 16 18",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M1.32226e-07 1.6609C7.22332e-08 0.907329 0.801887 0.424528 1.46789 0.777117L15.3306 8.11621C16.0401 8.49182 16.0401 9.50818 15.3306 9.88379L1.46789 17.2229C0.801886 17.5755 1.36076e-06 17.0927 1.30077e-06 16.3391L1.32226e-07 1.6609Z",fill:"currentColor"})))),Dm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 10 16",fill:"currentColor",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M4.25 9.25V13.5H5.75V9.25L10 9.25V7.75L5.75 7.75V3.5H4.25V7.75L0 7.75V9.25L4.25 9.25Z"})))),Am=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{width:25,height:25,viewBox:"0 0 25 25",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M10.2852 24.0745L13.7139 18.0742",stroke:"currentColor",strokeWidth:1.5625}),i.createElement("path",{d:"M14.5742 24.0749L17.1457 19.7891",stroke:"currentColor",strokeWidth:1.5625}),i.createElement("path",{d:"M19.4868 24.0735L20.7229 21.7523C21.3259 20.6143 21.5457 19.3122 21.3496 18.0394C21.1535 16.7666 20.5519 15.591 19.6342 14.6874L23.7984 6.87853C24.0123 6.47728 24.0581 6.00748 23.9256 5.57249C23.7932 5.1375 23.4933 4.77294 23.0921 4.55901C22.6908 4.34509 22.221 4.29932 21.7861 4.43178C21.3511 4.56424 20.9865 4.86408 20.7726 5.26533L16.6084 13.0742C15.3474 12.8142 14.0362 12.9683 12.8699 13.5135C11.7035 14.0586 10.7443 14.9658 10.135 16.1L6 24.0735",stroke:"currentColor",strokeWidth:1.5625}),i.createElement("path",{d:"M4 15L5 13L7 12L5 11L4 9L3 11L1 12L3 13L4 15Z",stroke:"currentColor",strokeWidth:1.5625,strokeLinejoin:"round"}),i.createElement("path",{d:"M11.5 8L12.6662 5.6662L15 4.5L12.6662 3.3338L11.5 1L10.3338 3.3338L8 4.5L10.3338 5.6662L11.5 8Z",stroke:"currentColor",strokeWidth:1.5625,strokeLinejoin:"round"})))),Im=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M4.75 9.25H1.25V12.75",stroke:"currentColor",strokeWidth:1,strokeLinecap:"square"}),i.createElement("path",{d:"M11.25 6.75H14.75V3.25",stroke:"currentColor",strokeWidth:1,strokeLinecap:"square"}),i.createElement("path",{d:"M14.1036 6.65539C13.8 5.27698 13.0387 4.04193 11.9437 3.15131C10.8487 2.26069 9.48447 1.76694 8.0731 1.75043C6.66173 1.73392 5.28633 2.19563 4.17079 3.0604C3.05526 3.92516 2.26529 5.14206 1.92947 6.513",stroke:"currentColor",strokeWidth:1}),i.createElement("path",{d:"M1.89635 9.34461C2.20001 10.723 2.96131 11.9581 4.05631 12.8487C5.15131 13.7393 6.51553 14.2331 7.9269 14.2496C9.33827 14.2661 10.7137 13.8044 11.8292 12.9396C12.9447 12.0748 13.7347 10.8579 14.0705 9.487",stroke:"currentColor",strokeWidth:1})))),Om=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 13 13",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("rect",{x:.6,y:.6,width:11.8,height:11.8,rx:5.9,stroke:"currentColor",strokeWidth:1.2}),i.createElement("path",{d:"M4.25 7.5C4.25 6 5.75 5 6.5 6.5C7.25 8 8.75 7 8.75 5.5",stroke:"currentColor",strokeWidth:1.2})))),Lm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 21 20",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{fillRule:"evenodd",clipRule:"evenodd",d:"M9.29186 1.92702C9.06924 1.82745 8.87014 1.68202 8.70757 1.50024L7.86631 0.574931C7.62496 0.309957 7.30773 0.12592 6.95791 0.0479385C6.60809 -0.0300431 6.24274 0.00182978 5.91171 0.139208C5.58068 0.276585 5.3001 0.512774 5.10828 0.815537C4.91645 1.1183 4.82272 1.47288 4.83989 1.83089L4.90388 3.08019C4.91612 3.32348 4.87721 3.56662 4.78968 3.79394C4.70215 4.02126 4.56794 4.2277 4.39571 4.39994C4.22347 4.57219 4.01704 4.7064 3.78974 4.79394C3.56243 4.88147 3.3193 4.92038 3.07603 4.90814L1.8308 4.84414C1.47162 4.82563 1.11553 4.91881 0.811445 5.11086C0.507359 5.30292 0.270203 5.58443 0.132561 5.91671C-0.00508149 6.249 -0.0364554 6.61576 0.0427496 6.9666C0.121955 7.31744 0.307852 7.63514 0.5749 7.87606L1.50016 8.71204C1.68193 8.87461 1.82735 9.07373 1.92692 9.29636C2.02648 9.51898 2.07794 9.76012 2.07794 10.004C2.07794 10.2479 2.02648 10.489 1.92692 10.7116C1.82735 10.9343 1.68193 11.1334 1.50016 11.296L0.5749 12.1319C0.309856 12.3729 0.125575 12.6898 0.0471809 13.0393C-0.0312128 13.3888 9.64098e-05 13.754 0.13684 14.0851C0.273583 14.4162 0.509106 14.6971 0.811296 14.8894C1.11349 15.0817 1.46764 15.1762 1.82546 15.1599L3.0707 15.0959C3.31397 15.0836 3.5571 15.1225 3.7844 15.2101C4.01171 15.2976 4.21814 15.4318 4.39037 15.6041C4.56261 15.7763 4.69682 15.9827 4.78435 16.2101C4.87188 16.4374 4.91078 16.6805 4.89855 16.9238L4.83455 18.1691C4.81605 18.5283 4.90921 18.8844 5.10126 19.1885C5.2933 19.4926 5.5748 19.7298 5.90707 19.8674C6.23934 20.0051 6.60608 20.0365 6.9569 19.9572C7.30772 19.878 7.6254 19.6921 7.86631 19.4251L8.7129 18.4998C8.87547 18.318 9.07458 18.1725 9.29719 18.073C9.51981 17.9734 9.76093 17.9219 10.0048 17.9219C10.2487 17.9219 10.4898 17.9734 10.7124 18.073C10.935 18.1725 11.1341 18.318 11.2967 18.4998L12.1326 19.4251C12.3735 19.6921 12.6912 19.878 13.042 19.9572C13.3929 20.0365 13.7596 20.0051 14.0919 19.8674C14.4241 19.7298 14.7056 19.4926 14.8977 19.1885C15.0897 18.8844 15.1829 18.5283 15.1644 18.1691L15.1004 16.9238C15.0882 16.6805 15.1271 16.4374 15.2146 16.2101C15.3021 15.9827 15.4363 15.7763 15.6086 15.6041C15.7808 15.4318 15.9872 15.2976 16.2145 15.2101C16.4418 15.1225 16.685 15.0836 16.9282 15.0959L18.1735 15.1599C18.5326 15.1784 18.8887 15.0852 19.1928 14.8931C19.4969 14.7011 19.7341 14.4196 19.8717 14.0873C20.0093 13.755 20.0407 13.3882 19.9615 13.0374C19.8823 12.6866 19.6964 12.3689 19.4294 12.1279L18.5041 11.292C18.3223 11.1294 18.1769 10.9303 18.0774 10.7076C17.9778 10.485 17.9263 10.2439 17.9263 10C17.9263 9.75612 17.9778 9.51499 18.0774 9.29236C18.1769 9.06973 18.3223 8.87062 18.5041 8.70804L19.4294 7.87206C19.6964 7.63114 19.8823 7.31344 19.9615 6.9626C20.0407 6.61176 20.0093 6.245 19.8717 5.91271C19.7341 5.58043 19.4969 5.29892 19.1928 5.10686C18.8887 4.91481 18.5326 4.82163 18.1735 4.84014L16.9282 4.90414C16.685 4.91638 16.4418 4.87747 16.2145 4.78994C15.9872 4.7024 15.7808 4.56818 15.6086 4.39594C15.4363 4.2237 15.3021 4.01726 15.2146 3.78994C15.1271 3.56262 15.0882 3.31948 15.1004 3.07619L15.1644 1.83089C15.1829 1.4717 15.0897 1.11559 14.8977 0.811487C14.7056 0.507385 14.4241 0.270217 14.0919 0.132568C13.7596 -0.00508182 13.3929 -0.0364573 13.042 0.0427519C12.6912 0.121961 12.3735 0.307869 12.1326 0.574931L11.2914 1.50024C11.1288 1.68202 10.9297 1.82745 10.7071 1.92702C10.4845 2.02659 10.2433 2.07805 9.99947 2.07805C9.7556 2.07805 9.51448 2.02659 9.29186 1.92702ZM14.3745 10C14.3745 12.4162 12.4159 14.375 9.99977 14.375C7.58365 14.375 5.625 12.4162 5.625 10C5.625 7.58375 7.58365 5.625 9.99977 5.625C12.4159 5.625 14.3745 7.58375 14.3745 10Z",fill:"currentColor"})))),Mm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M6.5782 1.07092C6.71096 0.643026 7.28904 0.643027 7.4218 1.07092L8.59318 4.84622C8.65255 5.03758 8.82284 5.16714 9.01498 5.16714L12.8056 5.16714C13.2353 5.16714 13.4139 5.74287 13.0663 6.00732L9.99962 8.34058C9.84418 8.45885 9.77913 8.66848 9.83851 8.85984L11.0099 12.6351C11.1426 13.063 10.675 13.4189 10.3274 13.1544L7.26069 10.8211C7.10524 10.7029 6.89476 10.7029 6.73931 10.8211L3.6726 13.1544C3.32502 13.4189 2.85735 13.063 2.99012 12.6351L4.16149 8.85984C4.22087 8.66848 4.15582 8.45885 4.00038 8.34058L0.933671 6.00732C0.586087 5.74287 0.764722 5.16714 1.19436 5.16714L4.98502 5.16714C5.17716 5.16714 5.34745 5.03758 5.40682 4.84622L6.5782 1.07092Z",fill:"currentColor",stroke:"currentColor"})))),Rm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M6.5782 1.07092C6.71096 0.643026 7.28904 0.643027 7.4218 1.07092L8.59318 4.84622C8.65255 5.03758 8.82284 5.16714 9.01498 5.16714L12.8056 5.16714C13.2353 5.16714 13.4139 5.74287 13.0663 6.00732L9.99962 8.34058C9.84418 8.45885 9.77913 8.66848 9.83851 8.85984L11.0099 12.6351C11.1426 13.063 10.675 13.4189 10.3274 13.1544L7.26069 10.8211C7.10524 10.7029 6.89476 10.7029 6.73931 10.8211L3.6726 13.1544C3.32502 13.4189 2.85735 13.063 2.99012 12.6351L4.16149 8.85984C4.22087 8.66848 4.15582 8.45885 4.00038 8.34058L0.933671 6.00732C0.586087 5.74287 0.764722 5.16714 1.19436 5.16714L4.98502 5.16714C5.17716 5.16714 5.34745 5.03758 5.40682 4.84622L6.5782 1.07092Z",stroke:"currentColor",strokeWidth:1.5})))),Fm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("rect",{width:16,height:16,rx:2,fill:"currentColor"})))),Pm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{width:"1em",height:"5em",xmlns:"http://www.w3.org/2000/svg",fillRule:"evenodd","aria-hidden":"true",viewBox:"0 0 23 23",style:{height:"1.5em"},clipRule:"evenodd","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("path",{d:"M19 24h-14c-1.104 0-2-.896-2-2v-17h-1v-2h6v-1.5c0-.827.673-1.5 1.5-1.5h5c.825 0 1.5.671 1.5 1.5v1.5h6v2h-1v17c0 1.104-.896 2-2 2zm0-19h-14v16.5c0 .276.224.5.5.5h13c.276 0 .5-.224.5-.5v-16.5zm-7 7.586l3.293-3.293 1.414 1.414-3.293 3.293 3.293 3.293-1.414 1.414-3.293-3.293-3.293 3.293-1.414-1.414 3.293-3.293-3.293-3.293 1.414-1.414 3.293 3.293zm2-10.586h-4v1h4v-1z",fill:"currentColor",strokeWidth:.25,stroke:"currentColor"})))),jm=Vm((({title:e,titleId:t,...n})=>i.createElement("svg",{height:"1em",viewBox:"0 0 13 13",fill:"none",xmlns:"http://www.w3.org/2000/svg","aria-labelledby":t,...n},e?i.createElement("title",{id:t},e):null,i.createElement("rect",{x:.6,y:.6,width:11.8,height:11.8,rx:5.9,stroke:"currentColor",strokeWidth:1.2}),i.createElement("rect",{x:5.5,y:5.5,width:2,height:2,rx:1,fill:"currentColor"}))));function Vm(e){const t=e.name.replace("Svg","").replaceAll(/([A-Z])/g," $1").trimStart().toLowerCase()+" icon",n=n=>{const r=h.c(2);let i;return r[0]!==n?(i=p.jsx(e,{title:t,...n}),r[0]=n,r[1]=i):i=r[1],i};return n.displayName=e.name,n}function Bm(e){var t,n,r="";if("string"==typeof e||"number"==typeof e)r+=e;else if("object"==typeof e)if(Array.isArray(e))for(t=0;t{const n=h.c(13);let r,i,o;n[0]!==t?(({isHidden:i,...r}=t),n[0]=t,n[1]=r,n[2]=i):(r=n[1],i=n[2]),n[3]===Symbol.for("react.memo_cache_sentinel")?(o={nonNull:!0,caller:Um},n[3]=o):o=n[3];const{headerEditor:s}=Qh(o),a=gh(r,Um);let l,c;n[4]!==s||n[5]!==i?(l=()=>{i||null==s||s.refresh()},c=[s,i],n[4]=s,n[5]=i,n[6]=l,n[7]=c):(l=n[6],c=n[7]),e.useEffect(l,c);const u=i&&"hidden";let d,f;return n[8]!==u?(d=$m("graphiql-editor",u),n[8]=u,n[9]=d):d=n[9],n[10]!==a||n[11]!==d?(f=p.jsx("div",{className:d,ref:a}),n[10]=a,n[11]=d,n[12]=f):f=n[12],f},Hm=Object.assign((t=>{var n;const r=h.c(14);let i;r[0]===Symbol.for("react.memo_cache_sentinel")?(i={width:null,height:null},r[0]=i):i=r[0];const[o,s]=e.useState(i),[a,l]=e.useState(null),c=e.useRef(null),u=null==(n=qm(t.token))?void 0:n.href;let d,f,m;r[1]!==u?(d=()=>{if(c.current)return u?void fetch(u,{method:"HEAD"}).then((e=>{l(e.headers.get("Content-Type"))})).catch((()=>{l(null)})):(s({width:null,height:null}),void l(null))},f=[u],r[1]=u,r[2]=d,r[3]=f):(d=r[2],f=r[3]),e.useEffect(d,f),r[4]!==o.height||r[5]!==o.width||r[6]!==a?(m=null!==o.width&&null!==o.height?p.jsxs("div",{children:[o.width,"x",o.height,null===a?null:" "+a]}):null,r[4]=o.height,r[5]=o.width,r[6]=a,r[7]=m):m=r[7];const g=m;let v,y,b;return r[8]===Symbol.for("react.memo_cache_sentinel")?(v=()=>{var e,t;s({width:(null==(e=c.current)?void 0:e.naturalWidth)??null,height:(null==(t=c.current)?void 0:t.naturalHeight)??null})},r[8]=v):v=r[8],r[9]!==u?(y=p.jsx("img",{onLoad:v,ref:c,src:u}),r[9]=u,r[10]=y):y=r[10],r[11]!==g||r[12]!==y?(b=p.jsxs("div",{children:[y,g]}),r[11]=g,r[12]=y,r[13]=b):b=r[13],b}),{shouldRender(e){const t=qm(e);return!!t&&function(e){return/\.(bmp|gif|jpe?g|png|svg|webp)$/.test(e.pathname)}(t)}});function qm(e){if("string"!==e.type)return;const t=e.string.slice(1).slice(0,-1).trim();try{return new URL(t,location.protocol+"//"+location.host)}catch{}}const Wm=e=>{const t=h.c(2),n=Th(e,Wm);let r;return t[0]!==n?(r=p.jsx("div",{className:"graphiql-editor",ref:n}),t[0]=n,t[1]=r):r=t[1],r};var zm,Gm={};var Km=function(){if(zm)return Gm;zm=1;var e=t;return Gm.createRoot=e.createRoot,Gm.hydrateRoot=e.hydrateRoot,Gm}();const Ym=Qm;function Qm(t,n){const r=h.c(17);let i;r[0]!==t?(i=void 0===t?{}:t,r[0]=t,r[1]=i):i=r[1];const{responseTooltip:o,editorTheme:s,keyMap:a}=i,l=void 0===s?$u:s,c=void 0===a?Uu:a,{fetchError:u,validationErrors:d}=nd(),f=n||Ym;let m;r[2]!==f?(m={nonNull:!0,caller:f},r[2]=f,r[3]=m):m=r[3];const{initialResponse:g,responseEditor:v,setResponseEditor:y}=Qh(m),b=e.useRef(null),E=e.useRef(o);let x,w,T,C,S,k;return r[4]!==o?(x=()=>{E.current=o},w=[o],r[4]=o,r[5]=x,r[6]=w):(x=r[5],w=r[6]),e.useEffect(x,w),r[7]!==l||r[8]!==g||r[9]!==y?(T=()=>{let e;return e=!0,qu([Promise.resolve().then((()=>mV)),Promise.resolve().then((()=>lV)),Promise.resolve().then((()=>LV)),Promise.resolve().then((()=>QV)),Promise.resolve().then((()=>CV)),Promise.resolve().then((()=>IV)),Promise.resolve().then((()=>PV)),Promise.resolve().then((()=>s$)),Promise.resolve().then((()=>dB))],{useCommonAddons:!1}).then((t=>{if(!e)return;const n=document.createElement("div"),r=Km.createRoot(n);t.registerHelper("info","graphql-results",((e,t,i,o)=>{const s=E.current,a=[s&&p.jsx(s,{pos:o,token:e}),Hm.shouldRender(e)&&p.jsx(Hm,{token:e},"image-preview")].filter(Xm);if(a.length)return r.render(a),n;r.unmount()}));const i=b.current;if(!i)return;const o=t(i,{value:g,lineWrapping:!0,readOnly:!0,theme:l,mode:"graphql-results",foldGutter:!0,gutters:["CodeMirror-foldgutter"],info:!0,extraKeys:Hu});y(o)})),()=>{e=!1}},C=[l,g,y],r[7]=l,r[8]=g,r[9]=y,r[10]=T,r[11]=C):(T=r[10],C=r[11]),e.useEffect(T,C),th(v,"keyMap",c),r[12]!==u||r[13]!==v||r[14]!==d?(S=()=>{u&&(null==v||v.setValue(u)),d.length&&(null==v||v.setValue(Ns(d)))},k=[v,u,d],r[12]=u,r[13]=v,r[14]=d,r[15]=S,r[16]=k):(S=r[15],k=r[16]),e.useEffect(S,k),b}function Xm(e){return Boolean(e)}const Jm=e=>{const t=h.c(2),n=Qm(e,Jm);let r;return t[0]!==n?(r=p.jsx("section",{className:"result-window","aria-label":"Result Window","aria-live":"polite","aria-atomic":"true",ref:n}),t[0]=n,t[1]=r):r=t[1],r},Zm=t=>{const n=h.c(13);let r,i,o;n[0]!==t?(({isHidden:i,...r}=t),n[0]=t,n[1]=r,n[2]=i):(r=n[1],i=n[2]),n[3]===Symbol.for("react.memo_cache_sentinel")?(o={nonNull:!0,caller:Zm},n[3]=o):o=n[3];const{variableEditor:s}=Qh(o),a=qh(r,Zm);let l,c;n[4]!==i||n[5]!==s?(l=()=>{i||null==s||s.refresh()},c=[s,i],n[4]=i,n[5]=s,n[6]=l,n[7]=c):(l=n[6],c=n[7]),e.useEffect(l,c);const u=i&&"hidden";let d,f;return n[8]!==u?(d=$m("graphiql-editor",u),n[8]=u,n[9]=d):d=n[9],n[10]!==a||n[11]!==d?(f=p.jsx("div",{className:d,ref:a}),n[10]=a,n[11]=d,n[12]=f):f=n[12],f};function eg(t){const n=h.c(31),{defaultSizeRelation:r,direction:i,initiallyHidden:o,onHiddenElementChange:s,sizeThresholdFirst:a,sizeThresholdSecond:l,storageKey:c}=t,u=void 0===r?1:r,d=void 0===a?100:a,f=void 0===l?100:l,p=Pu();let m;n[0]!==p||n[1]!==c?(m=Zp(500,(e=>{c&&p.set(c,e)})),n[0]=p,n[1]=c,n[2]=m):m=n[2];const g=m;let v;n[3]!==o||n[4]!==p||n[5]!==c?(v=()=>{const e=c&&p.get(c);return e===tg||"first"===o?"first":e===ng||"second"===o?"second":null},n[3]=o,n[4]=p,n[5]=c,n[6]=v):v=n[6];const[y,b]=e.useState(v);let E;n[7]!==y||n[8]!==s?(E=e=>{e!==y&&(b(e),null==s||s(e))},n[7]=y,n[8]=s,n[9]=E):E=n[9];const x=E,w=e.useRef(null),T=e.useRef(null),C=e.useRef(null),S=e.useRef(`${u}`);let k,_,N,D,A,I,O;return n[10]!==p||n[11]!==c?(k=()=>{const e=c&&p.get(c)||S.current;w.current&&(w.current.style.flex=e===tg||e===ng?S.current:e),C.current&&(C.current.style.flex="1")},n[10]=p,n[11]=c,n[12]=k):k=n[12],n[13]!==i||n[14]!==p||n[15]!==c?(_=[i,p,c],n[13]=i,n[14]=p,n[15]=c,n[16]=_):_=n[16],e.useEffect(k,_),n[17]!==y||n[18]!==p||n[19]!==c?(D=()=>{const e=e=>{const t="first"===e?w.current:C.current;if(t&&(t.style.left="-1000px",t.style.position="absolute",t.style.opacity="0",t.style.height="500px",t.style.width="500px",w.current)){const e=parseFloat(w.current.style.flex);(!Number.isFinite(e)||e<1)&&(w.current.style.flex="1")}},t=e=>{const t="first"===e?w.current:C.current;if(t&&(t.style.width="",t.style.height="",t.style.opacity="",t.style.position="",t.style.left="",c)){const e=p.get(c);w.current&&e!==tg&&e!==ng&&(w.current.style.flex=e||S.current)}};"first"===y?e("first"):t("first"),"second"===y?e("second"):t("second")},N=[y,p,c],n[17]=y,n[18]=p,n[19]=c,n[20]=N,n[21]=D):(N=n[20],D=n[21]),e.useEffect(D,N),n[22]!==i||n[23]!==x||n[24]!==d||n[25]!==f||n[26]!==g?(A=()=>{if(!T.current||!w.current||!C.current)return;const e=T.current,t=w.current,n=t.parentElement,r="horizontal"===i?"clientX":"clientY",o="horizontal"===i?"left":"top",s="horizontal"===i?"right":"bottom",a="horizontal"===i?"clientWidth":"clientHeight",l=function(i){if(!(i.target===i.currentTarget))return;i.preventDefault();const l=i[r]-e.getBoundingClientRect()[o],c=function(i){if(0===i.buttons)return u();const c=i[r]-n.getBoundingClientRect()[o]-l,p=n.getBoundingClientRect()[s]-i[r]+l-e[a];if(c{e.removeEventListener("mousedown",l),e.removeEventListener("dblclick",c)}},I=[i,x,d,f,g],n[22]=i,n[23]=x,n[24]=d,n[25]=f,n[26]=g,n[27]=A,n[28]=I):(A=n[27],I=n[28]),e.useEffect(A,I),n[29]!==y?(O={dragBarRef:T,hiddenElement:y,firstRef:w,setHiddenElement:b,secondRef:C},n[29]=y,n[30]=O):O=n[30],O}const tg="hide-first",ng="hide-second",rg=e.forwardRef(((e,t)=>{const n=h.c(6);let r,i;return n[0]!==e.className?(r=$m("graphiql-un-styled",e.className),n[0]=e.className,n[1]=r):r=n[1],n[2]!==e||n[3]!==t||n[4]!==r?(i=p.jsx("button",{...e,ref:t,className:r}),n[2]=e,n[3]=t,n[4]=r,n[5]=i):i=n[5],i}));rg.displayName="UnStyledButton";const ig=e.forwardRef(((e,t)=>{const n=h.c(7);let r,i;return n[0]!==e.className||n[1]!==e.state?(r=$m("graphiql-button",{success:"graphiql-button-success",error:"graphiql-button-error"}[e.state],e.className),n[0]=e.className,n[1]=e.state,n[2]=r):r=n[2],n[3]!==e||n[4]!==t||n[5]!==r?(i=p.jsx("button",{...e,ref:t,className:r}),n[3]=e,n[4]=t,n[5]=r,n[6]=i):i=n[6],i}));ig.displayName="Button";const og=e.forwardRef(((e,t)=>{const n=h.c(6);let r,i;return n[0]!==e.className?(r=$m("graphiql-button-group",e.className),n[0]=e.className,n[1]=r):r=n[1],n[2]!==e||n[3]!==t||n[4]!==r?(i=p.jsx("div",{...e,ref:t,className:r}),n[2]=e,n[3]=t,n[4]=r,n[5]=i):i=n[5],i}));function sg(e,t,{checkForDefaultPrevented:n=!0}={}){return function(r){if(null==e||e(r),!1===n||!r.defaultPrevented)return null==t?void 0:t(r)}}function ag(e,t){if("function"==typeof e)return e(t);null!=e&&(e.current=t)}function lg(...e){return t=>{let n=!1;const r=e.map((e=>{const r=ag(e,t);return n||"function"!=typeof r||(n=!0),r}));if(n)return()=>{for(let t=0;t{const t=n.map((e=>i.createContext(e)));return function(n){const r=(null==n?void 0:n[e])||t;return i.useMemo((()=>({[`__scope${e}`]:{...n,[e]:r}})),[n,r])}};return r.scopeName=e,[function(t,r){const o=i.createContext(r),s=n.length;n=[...n,r];const a=t=>{var n;const{scope:r,children:a,...l}=t,c=(null==(n=null==r?void 0:r[e])?void 0:n[s])||o,u=i.useMemo((()=>l),Object.values(l));return p.jsx(c.Provider,{value:u,children:a})};return a.displayName=t+"Provider",[a,function(n,a){var l;const c=(null==(l=null==a?void 0:a[e])?void 0:l[s])||o,u=i.useContext(c);if(u)return u;if(void 0!==r)return r;throw new Error(`\`${n}\` must be used within \`${t}\``)}]},dg(r,...t)]}function dg(...e){const t=e[0];if(1===e.length)return t;const n=()=>{const n=e.map((e=>({useScope:e(),scopeName:e.scopeName})));return function(e){const r=n.reduce(((t,{useScope:n,scopeName:r})=>({...t,...n(e)[`__scope${r}`]})),{});return i.useMemo((()=>({[`__scope${t.scopeName}`]:r})),[r])}};return n.scopeName=t.scopeName,n}og.displayName="ButtonGroup";var fg=(null==globalThis?void 0:globalThis.document)?i.useLayoutEffect:()=>{},pg=i[" useId ".trim().toString()]||(()=>{}),hg=0;function mg(e){const[t,n]=i.useState(pg());return fg((()=>{n((e=>e??String(hg++)))}),[e]),e||(t?`radix-${t}`:"")}var gg=i[" useInsertionEffect ".trim().toString()]||fg;function vg({prop:e,defaultProp:t,onChange:n=(()=>{}),caller:r}){const[o,s,a]=function({defaultProp:e,onChange:t}){const[n,r]=i.useState(e),o=i.useRef(n),s=i.useRef(t);return gg((()=>{s.current=t}),[t]),i.useEffect((()=>{var e;o.current!==n&&(null==(e=s.current)||e.call(s,n),o.current=n)}),[n,o]),[n,r,s]}({defaultProp:t,onChange:n}),l=void 0!==e,c=l?e:o;{const t=i.useRef(void 0!==e);i.useEffect((()=>{const e=t.current;if(e!==l){const t=e?"controlled":"uncontrolled",n=l?"controlled":"uncontrolled";console.warn(`${r} is changing from ${t} to ${n}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`)}t.current=l}),[l,r])}const u=i.useCallback((t=>{var n;if(l){const r=function(e){return"function"==typeof e}(t)?t(e):t;r!==e&&(null==(n=a.current)||n.call(a,r))}else s(t)}),[l,e,s,a]);return[c,u]}function yg(e){const t=bg(e),n=i.forwardRef(((e,n)=>{const{children:r,...o}=e,s=i.Children.toArray(r),a=s.find(wg);if(a){const e=a.props.children,r=s.map((t=>t===a?i.Children.count(e)>1?i.Children.only(null):i.isValidElement(e)?e.props.children:null:t));return p.jsx(t,{...o,ref:n,children:i.isValidElement(e)?i.cloneElement(e,void 0,r):null})}return p.jsx(t,{...o,ref:n,children:r})}));return n.displayName=`${e}.Slot`,n}function bg(e){const t=i.forwardRef(((e,t)=>{const{children:n,...r}=e;if(i.isValidElement(n)){const e=function(e){var t,n;let r=null==(t=Object.getOwnPropertyDescriptor(e.props,"ref"))?void 0:t.get,i=r&&"isReactWarning"in r&&r.isReactWarning;if(i)return e.ref;if(r=null==(n=Object.getOwnPropertyDescriptor(e,"ref"))?void 0:n.get,i=r&&"isReactWarning"in r&&r.isReactWarning,i)return e.props.ref;return e.props.ref||e.ref}(n),o=function(e,t){const n={...t};for(const r in t){const i=e[r],o=t[r];/^on[A-Z]/.test(r)?i&&o?n[r]=(...e)=>{o(...e),i(...e)}:i&&(n[r]=i):"style"===r?n[r]={...i,...o}:"className"===r&&(n[r]=[i,o].filter(Boolean).join(" "))}return{...e,...n}}(r,n.props);return n.type!==i.Fragment&&(o.ref=t?lg(t,e):e),i.cloneElement(n,o)}return i.Children.count(n)>1?i.Children.only(null):null}));return t.displayName=`${e}.SlotClone`,t}var Eg=Symbol("radix.slottable");function xg(e){const t=({children:e})=>p.jsx(p.Fragment,{children:e});return t.displayName=`${e}.Slottable`,t.__radixId=Eg,t}function wg(e){return i.isValidElement(e)&&"function"==typeof e.type&&"__radixId"in e.type&&e.type.__radixId===Eg}var Tg=["a","button","div","form","h2","h3","img","input","label","li","nav","ol","p","select","span","svg","ul"].reduce(((e,t)=>{const n=yg(`Primitive.${t}`),r=i.forwardRef(((e,r)=>{const{asChild:i,...o}=e,s=i?n:t;return"undefined"!=typeof window&&(window[Symbol.for("radix-ui")]=!0),p.jsx(s,{...o,ref:r})}));return r.displayName=`Primitive.${t}`,{...e,[t]:r}}),{});function Cg(e,t){e&&o.flushSync((()=>e.dispatchEvent(t)))}function Sg(e){const t=i.useRef(e);return i.useEffect((()=>{t.current=e})),i.useMemo((()=>(...e)=>{var n;return null==(n=t.current)?void 0:n.call(t,...e)}),[])}var kg,_g="dismissableLayer.update",Ng="dismissableLayer.pointerDownOutside",Dg="dismissableLayer.focusOutside",Ag=i.createContext({layers:new Set,layersWithOutsidePointerEventsDisabled:new Set,branches:new Set}),Ig=i.forwardRef(((e,t)=>{const{disableOutsidePointerEvents:n=!1,onEscapeKeyDown:r,onPointerDownOutside:o,onFocusOutside:s,onInteractOutside:a,onDismiss:l,...c}=e,u=i.useContext(Ag),[d,f]=i.useState(null),h=(null==d?void 0:d.ownerDocument)??(null==globalThis?void 0:globalThis.document),[,m]=i.useState({}),g=cg(t,(e=>f(e))),v=Array.from(u.layers),[y]=[...u.layersWithOutsidePointerEventsDisabled].slice(-1),b=v.indexOf(y),E=d?v.indexOf(d):-1,x=u.layersWithOutsidePointerEventsDisabled.size>0,w=E>=b,T=function(e,t=(null==globalThis?void 0:globalThis.document)){const n=Sg(e),r=i.useRef(!1),o=i.useRef((()=>{}));return i.useEffect((()=>{const e=e=>{if(e.target&&!r.current){let r=function(){Lg(Ng,n,i,{discrete:!0})};const i={originalEvent:e};"touch"===e.pointerType?(t.removeEventListener("click",o.current),o.current=r,t.addEventListener("click",o.current,{once:!0})):r()}else t.removeEventListener("click",o.current);r.current=!1},i=window.setTimeout((()=>{t.addEventListener("pointerdown",e)}),0);return()=>{window.clearTimeout(i),t.removeEventListener("pointerdown",e),t.removeEventListener("click",o.current)}}),[t,n]),{onPointerDownCapture:()=>r.current=!0}}((e=>{const t=e.target,n=[...u.branches].some((e=>e.contains(t)));w&&!n&&(null==o||o(e),null==a||a(e),e.defaultPrevented||null==l||l())}),h),C=function(e,t=(null==globalThis?void 0:globalThis.document)){const n=Sg(e),r=i.useRef(!1);return i.useEffect((()=>{const e=e=>{if(e.target&&!r.current){Lg(Dg,n,{originalEvent:e},{discrete:!1})}};return t.addEventListener("focusin",e),()=>t.removeEventListener("focusin",e)}),[t,n]),{onFocusCapture:()=>r.current=!0,onBlurCapture:()=>r.current=!1}}((e=>{const t=e.target;[...u.branches].some((e=>e.contains(t)))||(null==s||s(e),null==a||a(e),e.defaultPrevented||null==l||l())}),h);return function(e,t=(null==globalThis?void 0:globalThis.document)){const n=Sg(e);i.useEffect((()=>{const e=e=>{"Escape"===e.key&&n(e)};return t.addEventListener("keydown",e,{capture:!0}),()=>t.removeEventListener("keydown",e,{capture:!0})}),[n,t])}((e=>{E===u.layers.size-1&&(null==r||r(e),!e.defaultPrevented&&l&&(e.preventDefault(),l()))}),h),i.useEffect((()=>{if(d)return n&&(0===u.layersWithOutsidePointerEventsDisabled.size&&(kg=h.body.style.pointerEvents,h.body.style.pointerEvents="none"),u.layersWithOutsidePointerEventsDisabled.add(d)),u.layers.add(d),Og(),()=>{n&&1===u.layersWithOutsidePointerEventsDisabled.size&&(h.body.style.pointerEvents=kg)}}),[d,h,n,u]),i.useEffect((()=>()=>{d&&(u.layers.delete(d),u.layersWithOutsidePointerEventsDisabled.delete(d),Og())}),[d,u]),i.useEffect((()=>{const e=()=>m({});return document.addEventListener(_g,e),()=>document.removeEventListener(_g,e)}),[]),p.jsx(Tg.div,{...c,ref:g,style:{pointerEvents:x?w?"auto":"none":void 0,...e.style},onFocusCapture:sg(e.onFocusCapture,C.onFocusCapture),onBlurCapture:sg(e.onBlurCapture,C.onBlurCapture),onPointerDownCapture:sg(e.onPointerDownCapture,T.onPointerDownCapture)})}));Ig.displayName="DismissableLayer";function Og(){const e=new CustomEvent(_g);document.dispatchEvent(e)}function Lg(e,t,n,{discrete:r}){const i=n.originalEvent.target,o=new CustomEvent(e,{bubbles:!1,cancelable:!0,detail:n});t&&i.addEventListener(e,t,{once:!0}),r?Cg(i,o):i.dispatchEvent(o)}i.forwardRef(((e,t)=>{const n=i.useContext(Ag),r=i.useRef(null),o=cg(t,r);return i.useEffect((()=>{const e=r.current;if(e)return n.branches.add(e),()=>{n.branches.delete(e)}}),[n.branches]),p.jsx(Tg.div,{...e,ref:o})})).displayName="DismissableLayerBranch";var Mg="focusScope.autoFocusOnMount",Rg="focusScope.autoFocusOnUnmount",Fg={bubbles:!1,cancelable:!0},Pg=i.forwardRef(((e,t)=>{const{loop:n=!1,trapped:r=!1,onMountAutoFocus:o,onUnmountAutoFocus:s,...a}=e,[l,c]=i.useState(null),u=Sg(o),d=Sg(s),f=i.useRef(null),h=cg(t,(e=>c(e))),m=i.useRef({paused:!1,pause(){this.paused=!0},resume(){this.paused=!1}}).current;i.useEffect((()=>{if(r){let e=function(e){if(m.paused||!l)return;const t=e.target;l.contains(t)?f.current=t:$g(f.current,{select:!0})},t=function(e){if(m.paused||!l)return;const t=e.relatedTarget;null!==t&&(l.contains(t)||$g(f.current,{select:!0}))},n=function(e){if(document.activeElement===document.body)for(const t of e)t.removedNodes.length>0&&$g(l)};document.addEventListener("focusin",e),document.addEventListener("focusout",t);const r=new MutationObserver(n);return l&&r.observe(l,{childList:!0,subtree:!0}),()=>{document.removeEventListener("focusin",e),document.removeEventListener("focusout",t),r.disconnect()}}}),[r,l,m.paused]),i.useEffect((()=>{if(l){Ug.add(m);const t=document.activeElement;if(!l.contains(t)){const n=new CustomEvent(Mg,Fg);l.addEventListener(Mg,u),l.dispatchEvent(n),n.defaultPrevented||(!function(e,{select:t=!1}={}){const n=document.activeElement;for(const r of e)if($g(r,{select:t}),document.activeElement!==n)return}((e=jg(l),e.filter((e=>"A"!==e.tagName))),{select:!0}),document.activeElement===t&&$g(l))}return()=>{l.removeEventListener(Mg,u),setTimeout((()=>{const e=new CustomEvent(Rg,Fg);l.addEventListener(Rg,d),l.dispatchEvent(e),e.defaultPrevented||$g(t??document.body,{select:!0}),l.removeEventListener(Rg,d),Ug.remove(m)}),0)}}var e}),[l,u,d,m]);const g=i.useCallback((e=>{if(!n&&!r)return;if(m.paused)return;const t="Tab"===e.key&&!e.altKey&&!e.ctrlKey&&!e.metaKey,i=document.activeElement;if(t&&i){const t=e.currentTarget,[r,o]=function(e){const t=jg(e),n=Vg(t,e),r=Vg(t.reverse(),e);return[n,r]}(t);r&&o?e.shiftKey||i!==o?e.shiftKey&&i===r&&(e.preventDefault(),n&&$g(o,{select:!0})):(e.preventDefault(),n&&$g(r,{select:!0})):i===t&&e.preventDefault()}}),[n,r,m.paused]);return p.jsx(Tg.div,{tabIndex:-1,...a,ref:h,onKeyDown:g})}));function jg(e){const t=[],n=document.createTreeWalker(e,NodeFilter.SHOW_ELEMENT,{acceptNode:e=>{const t="INPUT"===e.tagName&&"hidden"===e.type;return e.disabled||e.hidden||t?NodeFilter.FILTER_SKIP:e.tabIndex>=0?NodeFilter.FILTER_ACCEPT:NodeFilter.FILTER_SKIP}});for(;n.nextNode();)t.push(n.currentNode);return t}function Vg(e,t){for(const n of e)if(!Bg(n,{upTo:t}))return n}function Bg(e,{upTo:t}){if("hidden"===getComputedStyle(e).visibility)return!0;for(;e;){if(void 0!==t&&e===t)return!1;if("none"===getComputedStyle(e).display)return!0;e=e.parentElement}return!1}function $g(e,{select:t=!1}={}){if(e&&e.focus){const n=document.activeElement;e.focus({preventScroll:!0}),e!==n&&function(e){return e instanceof HTMLInputElement&&"select"in e}(e)&&t&&e.select()}}Pg.displayName="FocusScope";var Ug=function(){let e=[];return{add(t){const n=e[0];t!==n&&(null==n||n.pause()),e=Hg(e,t),e.unshift(t)},remove(t){var n;e=Hg(e,t),null==(n=e[0])||n.resume()}}}();function Hg(e,t){const n=[...e],r=n.indexOf(t);return-1!==r&&n.splice(r,1),n}var qg=i.forwardRef(((e,n)=>{var r;const{container:o,...s}=e,[a,l]=i.useState(!1);fg((()=>l(!0)),[]);const c=o||a&&(null==(r=null==globalThis?void 0:globalThis.document)?void 0:r.body);return c?t.createPortal(p.jsx(Tg.div,{...s,ref:n}),c):null}));qg.displayName="Portal";var Wg=e=>{const{present:t,children:n}=e,r=function(e){const[t,n]=i.useState(),r=i.useRef(null),o=i.useRef(e),s=i.useRef("none"),a=e?"mounted":"unmounted",[l,c]=function(e,t){return i.useReducer(((e,n)=>t[e][n]??e),e)}(a,{mounted:{UNMOUNT:"unmounted",ANIMATION_OUT:"unmountSuspended"},unmountSuspended:{MOUNT:"mounted",ANIMATION_END:"unmounted"},unmounted:{MOUNT:"mounted"}});return i.useEffect((()=>{const e=zg(r.current);s.current="mounted"===l?e:"none"}),[l]),fg((()=>{const t=r.current,n=o.current;if(n!==e){const r=s.current,i=zg(t);if(e)c("MOUNT");else if("none"===i||"none"===(null==t?void 0:t.display))c("UNMOUNT");else{c(n&&r!==i?"ANIMATION_OUT":"UNMOUNT")}o.current=e}}),[e,c]),fg((()=>{if(t){let e;const n=t.ownerDocument.defaultView??window,i=i=>{const s=zg(r.current).includes(i.animationName);if(i.target===t&&s&&(c("ANIMATION_END"),!o.current)){const r=t.style.animationFillMode;t.style.animationFillMode="forwards",e=n.setTimeout((()=>{"forwards"===t.style.animationFillMode&&(t.style.animationFillMode=r)}))}},a=e=>{e.target===t&&(s.current=zg(r.current))};return t.addEventListener("animationstart",a),t.addEventListener("animationcancel",i),t.addEventListener("animationend",i),()=>{n.clearTimeout(e),t.removeEventListener("animationstart",a),t.removeEventListener("animationcancel",i),t.removeEventListener("animationend",i)}}c("ANIMATION_END")}),[t,c]),{isPresent:["mounted","unmountSuspended"].includes(l),ref:i.useCallback((e=>{r.current=e?getComputedStyle(e):null,n(e)}),[])}}(t),o="function"==typeof n?n({present:r.isPresent}):i.Children.only(n),s=cg(r.ref,function(e){var t,n;let r=null==(t=Object.getOwnPropertyDescriptor(e.props,"ref"))?void 0:t.get,i=r&&"isReactWarning"in r&&r.isReactWarning;if(i)return e.ref;if(r=null==(n=Object.getOwnPropertyDescriptor(e,"ref"))?void 0:n.get,i=r&&"isReactWarning"in r&&r.isReactWarning,i)return e.props.ref;return e.props.ref||e.ref}(o));return"function"==typeof n||r.isPresent?i.cloneElement(o,{ref:s}):null};function zg(e){return(null==e?void 0:e.animationName)||"none"}Wg.displayName="Presence";var Gg=0;function Kg(){i.useEffect((()=>{const e=document.querySelectorAll("[data-radix-focus-guard]");return document.body.insertAdjacentElement("afterbegin",e[0]??Yg()),document.body.insertAdjacentElement("beforeend",e[1]??Yg()),Gg++,()=>{1===Gg&&document.querySelectorAll("[data-radix-focus-guard]").forEach((e=>e.remove())),Gg--}}),[])}function Yg(){const e=document.createElement("span");return e.setAttribute("data-radix-focus-guard",""),e.tabIndex=0,e.style.outline="none",e.style.opacity="0",e.style.position="fixed",e.style.pointerEvents="none",e}var Qg=function(){return Qg=Object.assign||function(e){for(var t,n=1,r=arguments.length;n