diff --git a/.github/workflows/run-ci-cd.yaml b/.github/workflows/run-ci-cd.yaml index d8ebb2311b..3a67f3a2ce 100644 --- a/.github/workflows/run-ci-cd.yaml +++ b/.github/workflows/run-ci-cd.yaml @@ -146,7 +146,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Setup Trivy - uses: aquasecurity/setup-trivy@e6c2c5e321ed9123bda567646e2f96565e34abe1 + uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 with: cache: true @@ -172,7 +172,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Setup Trivy - uses: aquasecurity/setup-trivy@e6c2c5e321ed9123bda567646e2f96565e34abe1 + uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 with: cache: true @@ -260,6 +260,29 @@ jobs: permissions: contents: read runs-on: ubuntu-latest + services: + db: + image: pgvector/pgvector:pg16 + env: + POSTGRES_DB: nest_db_e2e + POSTGRES_PASSWORD: nest_user_e2e_password + POSTGRES_USER: nest_user_e2e + options: >- + --health-cmd="pg_isready -U nest_user_e2e -d nest_db_e2e -h localhost -p 5432" + --health-interval=5s + --health-timeout=5s + --health-retries=5 + ports: + - 5432:5432 + cache: + image: redis:8.0.5-alpine3.21 + options: >- + --health-cmd="redis-cli ping" + --health-interval=5s + --health-retries=5 + --health-timeout=5s + ports: + - 6379:6379 steps: - name: Check out repository uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 @@ -267,6 +290,43 @@ jobs: - name: Set up Docker buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f + - name: Setup Backend environment + uses: ./.github/workflows/setup-backend-environment + with: + db_username: nest_user_e2e + db_name: nest_db_e2e + + - name: Start Backend in the background + run: | + docker run -d --rm --name e2e-nest-backend \ + --env-file backend/.env.e2e.example \ + --network host \ + -e DJANGO_DB_HOST=localhost \ + -e DJANGO_REDIS_AUTH_ENABLED=False \ + -e DJANGO_REDIS_HOST=localhost \ + -p 9000:9000 \ + owasp/nest:test-backend-latest \ + sh -c ' + python manage.py migrate && + gunicorn wsgi:application --bind 0.0.0.0:9000 + ' + + - name: Waiting for the backend to be ready + run: | + timeout 5m bash -c ' + until wget --spider http://localhost:9000/a; do + echo "Waiting for backend..." + sleep 5 + done + ' + echo "Backend is up!" + + - name: Load Postgres data + env: + PGPASSWORD: nest_user_e2e_password + run: | + pg_restore -h localhost -U nest_user_e2e -d nest_db_e2e < backend/data/nest.dump + - name: Build frontend end-to-end testing image uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 with: @@ -281,7 +341,7 @@ jobs: - name: Run frontend end-to-end tests run: | - docker run --env-file frontend/.env.example owasp/nest:test-frontend-e2e-latest pnpm run test:e2e + docker run --env-file frontend/.env.e2e.example owasp/nest:test-frontend-e2e-latest pnpm run test:e2e timeout-minutes: 10 run-frontend-a11y-tests: @@ -335,6 +395,25 @@ jobs: fi timeout-minutes: 5 + run-graphql-fuzz-tests: + name: Run GraphQL fuzz tests + needs: + - scan-code + - scan-ci-dependencies + uses: ./.github/workflows/run-fuzz-tests.yaml + with: + test-file: graphql_test.py + + run-rest-fuzz-tests: + name: Run REST fuzz tests + needs: + - scan-code + - scan-ci-dependencies + uses: ./.github/workflows/run-fuzz-tests.yaml + with: + test-file: rest_test.py + rest-url: http://localhost:9500/api/v0 + build-staging-images: name: Build Staging Images env: @@ -467,7 +546,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Setup Trivy - uses: aquasecurity/setup-trivy@e6c2c5e321ed9123bda567646e2f96565e34abe1 + uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 with: cache: true @@ -833,7 +912,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 - name: Setup Trivy - uses: aquasecurity/setup-trivy@e6c2c5e321ed9123bda567646e2f96565e34abe1 + uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 with: cache: true diff --git a/.github/workflows/run-fuzz-tests.yaml b/.github/workflows/run-fuzz-tests.yaml new file mode 100644 index 0000000000..0de9a396b2 --- /dev/null +++ b/.github/workflows/run-fuzz-tests.yaml @@ -0,0 +1,118 @@ +name: Run fuzz tests + +on: + workflow_call: + inputs: + test-file: + description: 'The test file to run fuzz tests on' + required: true + type: string + rest-url: + description: 'The REST API URL to test against' + required: false + type: string + default: 'http://localhost:9500/api/v0' + +jobs: + run-fuzz-tests: + name: Run Fuzz Tests + runs-on: ubuntu-latest + timeout-minutes: 20 + services: + db: + image: pgvector/pgvector:pg16 + env: + POSTGRES_DB: nest_db_fuzz + POSTGRES_PASSWORD: nest_user_fuzz_password + POSTGRES_USER: nest_user_fuzz + options: >- + --health-cmd="pg_isready -U nest_user_fuzz -d nest_db_fuzz -h localhost -p 5432" + --health-interval=5s + --health-retries=5 + --health-timeout=5s + ports: + - 5432:5432 + cache: + image: redis:8.0.5-alpine3.21 + options: >- + --health-cmd="redis-cli ping" + --health-interval=5s + --health-retries=5 + --health-timeout=5s + ports: + - 6379:6379 + steps: + - name: Check out repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Docker buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f + + - name: Setup Backend environment + uses: ./.github/workflows/setup-backend-environment + with: + db_username: nest_user_fuzz + db_name: nest_db_fuzz + + - name: Run backend with fuzz environment variables + run: | + docker run -d --rm --name fuzz-nest-backend \ + --env-file backend/.env.fuzz.example \ + --network host \ + -e DJANGO_DB_HOST=localhost \ + -e DJANGO_REDIS_AUTH_ENABLED=False \ + -e DJANGO_REDIS_HOST=localhost \ + -p 9500:9500 \ + owasp/nest:test-backend-latest \ + sh -c ' + python manage.py migrate && + gunicorn wsgi:application --bind 0.0.0.0:9500 + ' + + - name: Waiting for the backend to be ready + run: | + timeout 5m bash -c ' + until wget --spider http://localhost:9500/a; do + echo "Waiting for backend..." + sleep 5 + done + ' + echo "Backend is up!" + + - name: Load Postgres data + env: + PGPASSWORD: nest_user_fuzz_password + run: | + set -euo pipefail + if ! pg_restore -h localhost -U nest_user_fuzz -d nest_db_fuzz < backend/data/nest.dump; then + echo "Data loading failed" + exit 1 + fi + echo "Data loading completed." + + - name: Build Fuzz-testing image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + with: + cache-from: | + type=gha + type=registry,ref=owasp/nest:test-fuzz-backend-cache + cache-to: | + type=gha,compression=zstd + context: backend + file: docker/backend/Dockerfile.fuzz + load: true + platforms: linux/amd64 + tags: owasp/nest:test-fuzz-backend-latest + + - name: Run fuzz tests + env: + TEST_FILE: ${{ inputs.test-file }} + REST_URL: ${{ inputs.rest-url }} + run: | + docker run \ + --network host \ + -e BASE_URL=http://localhost:9500 \ + -e CI=true \ + -e REST_URL="$REST_URL" \ + -e TEST_FILE="$TEST_FILE" \ + owasp/nest:test-fuzz-backend-latest diff --git a/.github/workflows/setup-backend-environment/action.yaml b/.github/workflows/setup-backend-environment/action.yaml new file mode 100644 index 0000000000..5fcf9b9923 --- /dev/null +++ b/.github/workflows/setup-backend-environment/action.yaml @@ -0,0 +1,44 @@ +name: Set up Backend environment + +description: Sets up the Backend environment testing. + +inputs: + db_username: + description: 'Database username' + required: true + db_name: + description: 'Database name' + required: true + +runs: + using: composite + steps: + - name: Wait for database to be ready + env: + DB_USERNAME: ${{ inputs.db_username }} + DB_NAME: ${{ inputs.db_name }} + run: | + timeout 5m bash -c ' + until docker exec ${{ job.services.db.id }} pg_isready -U $DB_USERNAME -d $DB_NAME; do + echo "Waiting for database..." + sleep 5 + done + ' + shell: bash + + - name: Install PostgreSQL client + run: sudo apt-get install -y postgresql-client + shell: bash + + - name: Build backend image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + with: + cache-from: | + type=gha + cache-to: | + type=gha,compression=zstd + context: backend + file: docker/backend/Dockerfile + load: true + platforms: linux/amd64 + tags: owasp/nest:test-backend-latest diff --git a/.github/workflows/update-nest-test-images.yaml b/.github/workflows/update-nest-test-images.yaml index e56ce330ed..8c0bb4a0c8 100644 --- a/.github/workflows/update-nest-test-images.yaml +++ b/.github/workflows/update-nest-test-images.yaml @@ -74,4 +74,17 @@ jobs: platforms: linux/amd64 push: true tags: owasp/nest:test-frontend-e2e-latest + + - name: Build and push fuzz-test-backend image + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 + with: + cache-from: type=registry,ref=owasp/nest:test-fuzz-backend-cache + cache-to: | + type=gha,compression=zstd + type=registry,ref=owasp/nest:test-fuzz-backend-cache + context: backend + file: docker/backend/Dockerfile.fuzz + platforms: linux/amd64 + push: true + tags: owasp/nest:test-fuzz-backend-latest timeout-minutes: 15 diff --git a/.gitignore b/.gitignore index b67bbd5b5b..23c5777f4c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,9 +4,12 @@ __pycache__ .cache .coverage .cursor/rules/snyk_rules.mdc +backend/fuzzing_results/ .DS_Store .env* !.env.example +!.env.e2e.example +!.env.fuzz.example .github/instructions/snyk_rules.instructions.md .idea .lighthouseci/ @@ -55,3 +58,6 @@ logs node_modules/ TODO venv/ + +# Snyk Security Extension - AI Rules (auto-generated) +.cursor/rules/snyk_rules.mdc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1e66e87172..e974cea4a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - --args=--config=__GIT_WORKING_DIR__/infrastructure/.tflint.hcl - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.11 + rev: v0.14.13 hooks: - id: ruff-check args: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53b8936f86..feaea6a6a1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -275,6 +275,8 @@ Ensure that all `.env` files are saved in **UTF-8 format without BOM (Byte Order 1. **Load Initial Data**: + - Make sure you have `gzip` installed on your machine. + - Open a new terminal session and run the following command to populate the database with initial data from fixtures: ```bash @@ -404,6 +406,60 @@ make test This command runs tests and checks that coverage threshold requirements are satisfied for both backend and frontend. **Please note your PR won't be merged if it fails the code tests checks.** +### Running e2e Tests + +Run the frontend e2e tests with the following command: + +```bash +make test-frontend-e2e +``` + +This command automatically: + +- Starts the database and backend containers +- Runs migrations and loads test data +- Executes the e2e tests +- Cleans up containers when done + +For debugging, you can run the e2e backend separately: + +```bash +make run-backend-e2e +``` + +Then load data manually in another terminal: + +```bash +make load-data-e2e +``` + +### Running Fuzz Tests + +Run the fuzz tests with the following command: + +```bash +make test-fuzz +``` + +This command automatically: + +- Starts the database and backend containers +- Runs migrations and loads test data +- Executes the fuzz tests +- Cleans up containers when done + +For debugging, you can run the fuzz backend separately: + +```bash +make run-backend-fuzz +``` + +Then load data manually in another terminal: + +```bash +make load-data-fuzz +``` + ### Test Coverage - There is a **minimum test coverage requirement** for the **backend** code -- see [pyproject.toml](https://github.com/OWASP/Nest/blob/main/backend/pyproject.toml). @@ -492,9 +548,40 @@ flowchart TD linkStyle 24 stroke:#f44336,stroke-width:2px ``` +### Keep Your Fork in Sync with Upstream + +To avoid working on an outdated copy of Nest (and to reduce merge conflicts), contributors may find it helpful to keep their fork synchronized with the main OWASP Nest repository. + +
+Setting up the upstream remote + +If you haven't added the upstream remote yet, add it using: + +```bash +git remote add upstream https://github.com/OWASP/Nest.git +``` + +Verify that the upstream remote has been added by running: + +```bash +git remote -v +``` + +This should show both `origin` (your fork) and `upstream` (the main repository) remotes. + +
+ +Before working on a **new** feature or issue, update your local `main` branch from `upstream/main`: + +```bash +git checkout main +git fetch upstream +git merge upstream/main +``` + ### 1. Find Something to Work On -- Check the **Issues** tab for open issues: [https://github.com/owasp/nest/issues](https://github.com/owasp/nest/issues) +- Check the **Issues** tab for open issues: [https://github.com/OWASP/Nest/issues](https://github.com/OWASP/Nest/issues) - Found a bug or have a feature request? Open a new issue. - Want to work on an existing issue? Ask the maintainers to assign it to you before submitting a pull request. - New to the project? Start with issues labeled `good first issue` for an easier onboarding experience. diff --git a/backend/.env.e2e.example b/backend/.env.e2e.example new file mode 100644 index 0000000000..9b8e9d05ea --- /dev/null +++ b/backend/.env.e2e.example @@ -0,0 +1,24 @@ +DJANGO_ALGOLIA_APPLICATION_ID=None +DJANGO_ALGOLIA_EXCLUDED_LOCAL_INDEX_NAMES=None +DJANGO_ALGOLIA_WRITE_API_KEY=None +DJANGO_ALLOWED_HOSTS=* +DJANGO_AWS_ACCESS_KEY_ID=None +DJANGO_AWS_SECRET_ACCESS_KEY=None +DJANGO_SETTINGS_MODULE=settings.e2e +DJANGO_CONFIGURATION=E2E +DJANGO_DB_HOST=db +DJANGO_DB_NAME=nest_db_e2e +DJANGO_DB_USER=nest_user_e2e +DJANGO_DB_PASSWORD=nest_user_e2e_password +DJANGO_DB_PORT=5432 +DJANGO_OPEN_AI_SECRET_KEY=None +DJANGO_PUBLIC_IP_ADDRESS="127.0.0.1" +DJANGO_REDIS_AUTH_ENABLED=True +DJANGO_REDIS_HOST=cache +DJANGO_REDIS_PASSWORD=nest-cache-e2e-password +DJANGO_RELEASE_VERSION=None +DJANGO_SECRET_KEY=None +DJANGO_SENTRY_DSN=None +DJANGO_SLACK_BOT_TOKEN=None +DJANGO_SLACK_SIGNING_SECRET=None +GITHUB_TOKEN=None diff --git a/backend/.env.example b/backend/.env.example index 193d9df930..e1e27c8422 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -13,6 +13,7 @@ DJANGO_DB_USER=None DJANGO_ELEVENLABS_API_KEY=None DJANGO_OPEN_AI_SECRET_KEY=None DJANGO_PUBLIC_IP_ADDRESS="127.0.0.1" +DJANGO_REDIS_AUTH_ENABLED=True DJANGO_REDIS_HOST=None DJANGO_REDIS_PASSWORD=None DJANGO_RELEASE_VERSION=None diff --git a/backend/.env.fuzz.example b/backend/.env.fuzz.example new file mode 100644 index 0000000000..be2abbaa26 --- /dev/null +++ b/backend/.env.fuzz.example @@ -0,0 +1,24 @@ +DJANGO_ALGOLIA_APPLICATION_ID=None +DJANGO_ALGOLIA_EXCLUDED_LOCAL_INDEX_NAMES=None +DJANGO_ALGOLIA_WRITE_API_KEY=None +DJANGO_ALLOWED_HOSTS=* +DJANGO_AWS_ACCESS_KEY_ID=None +DJANGO_AWS_SECRET_ACCESS_KEY=None +DJANGO_SETTINGS_MODULE=settings.fuzz +DJANGO_CONFIGURATION=Fuzz +DJANGO_DB_HOST=db +DJANGO_DB_NAME=nest_db_fuzz +DJANGO_DB_USER=nest_user_fuzz +DJANGO_DB_PASSWORD=nest_user_fuzz_password +DJANGO_DB_PORT=5432 +DJANGO_OPEN_AI_SECRET_KEY=None +DJANGO_PUBLIC_IP_ADDRESS="127.0.0.1" +DJANGO_REDIS_AUTH_ENABLED=True +DJANGO_REDIS_HOST=cache +DJANGO_REDIS_PASSWORD=nest-fuzz-cache-password +DJANGO_RELEASE_VERSION=None +DJANGO_SECRET_KEY=None +DJANGO_SENTRY_DSN=None +DJANGO_SLACK_BOT_TOKEN=None +DJANGO_SLACK_SIGNING_SECRET=None +GITHUB_TOKEN=None diff --git a/backend/Makefile b/backend/Makefile index 8a470d8be8..0fd0084621 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -49,9 +49,36 @@ else @docker exec -it nest-backend $(CMD) 2>/dev/null endif +exec-backend-command-e2e: + @docker exec -i e2e-nest-backend $(CMD) + +exec-backend-command-e2e-it: + @docker exec -it e2e-nest-backend $(CMD) + +exec-backend-command-fuzz: + @docker exec -i fuzz-nest-backend $(CMD) + +exec-backend-command-fuzz-it: + @docker exec -it fuzz-nest-backend $(CMD) + +exec-db-command: + @docker exec -i nest-db $(CMD) + exec-db-command-it: @docker exec -it nest-db $(CMD) +exec-db-command-e2e: + @docker exec -i e2e-nest-db $(CMD) + +exec-db-command-e2e-it: + @docker exec -it e2e-nest-db $(CMD) + +exec-db-command-fuzz: + @docker exec -i fuzz-nest-db $(CMD) + +exec-db-command-fuzz-it: + @docker exec -it fuzz-nest-db $(CMD) + clear-cache: @CMD="python manage.py clear_cache" $(MAKE) exec-backend-command @@ -63,18 +90,7 @@ django-shell: dump-data: @echo "Dumping Nest data" - @CMD="python manage.py dumpdata \ - github \ - owasp \ - slack.Conversation \ - slack.Member \ - slack.Message \ - slack.Workspace \ - --indent=4 \ - --natural-foreign \ - --natural-primary -o data/nest.json" $(MAKE) exec-backend-command - @CMD="sed -E -i 's/(\"[^\"]*email\"): *\"([^\"]|\\\")*\"/\1: \"\"/g' data/nest.json" $(MAKE) exec-backend-command - @CMD="gzip -f data/nest.json" $(MAKE) exec-backend-command + @CMD="python manage.py dump_data" $(MAKE) exec-backend-command-it enrich-data: \ github-enrich-issues \ @@ -94,7 +110,15 @@ index-data: load-data: @echo "Loading Nest data" - @CMD="python manage.py load_data" $(MAKE) exec-backend-command + @CMD="pg_restore -U nest_user_dev -d nest_db_dev < ./backend/data/nest.dump" $(MAKE) exec-db-command + +load-data-e2e: + @echo "Loading Nest e2e data" + @CMD="pg_restore -U nest_user_e2e -d nest_db_e2e < ./backend/data/nest.dump" $(MAKE) exec-db-command-e2e + +load-data-fuzz: + @echo "Loading Nest fuzz data" + @CMD="pg_restore -U nest_user_fuzz -d nest_db_fuzz < ./backend/data/nest.dump" $(MAKE) exec-db-command-fuzz merge-migrations: @CMD="python manage.py makemigrations --merge" $(MAKE) exec-backend-command @@ -105,9 +129,18 @@ migrate: migrations: @CMD="python manage.py makemigrations" $(MAKE) exec-backend-command +migrations-empty: + @CMD="python manage.py makemigrations --empty $(APP_NAME)" $(MAKE) exec-backend-command + purge-data: @CMD="python manage.py purge_data" $(MAKE) exec-backend-command +purge-data-e2e: + @CMD="python manage.py purge_data" $(MAKE) exec-backend-command-e2e + +purge-data-fuzz: + @CMD="python manage.py purge_data" $(MAKE) exec-backend-command-fuzz + recreate-schema: @echo "Recreating Nest schema" @CMD="psql -U nest_user_dev -d nest_db_dev -c \ @@ -119,6 +152,14 @@ restore-backup: @echo "Restoring Nest backup" @CMD="python manage.py restore_backup" $(MAKE) exec-backend-command +run-backend-e2e: + @DOCKER_BUILDKIT=1 \ + docker compose --project-name nest-e2e -f docker-compose/e2e/compose.yaml up --build --remove-orphans --abort-on-container-exit backend db cache + +run-backend-fuzz: + @COMPOSE_BAKE=true DOCKER_BUILDKIT=1 \ + docker compose --project-name nest-fuzz -f docker-compose/fuzz/compose.yaml up --build --remove-orphans --abort-on-container-exit backend db cache + save-backup: @echo "Saving Nest backup" @CMD="python manage.py dumpdata --natural-primary --natural-foreign --indent=2" $(MAKE) exec-backend-command > backend/data/backup.json @@ -153,6 +194,18 @@ test-backend: --env-file backend/.env.example \ --rm nest-test-backend pytest +test-fuzz: + @docker container rm -f fuzz-nest-db >/dev/null 2>&1 || true + @docker volume rm -f nest-fuzz_fuzz-db-data >/dev/null 2>&1 || true + @COMPOSE_BAKE=true DOCKER_BUILDKIT=1 \ + docker compose --project-name nest-fuzz -f docker-compose/fuzz/compose.yaml up --build --remove-orphans --abort-on-container-exit db cache backend data-loader + @echo "Running REST API fuzz tests..." + @COMPOSE_BAKE=true DOCKER_BUILDKIT=1 \ + docker compose --project-name nest-fuzz -f docker-compose/fuzz/compose.yaml up --build --remove-orphans --abort-on-container-exit db backend rest + @echo "Running GraphQL fuzz tests..." + @COMPOSE_BAKE=true DOCKER_BUILDKIT=1 \ + docker compose --project-name nest-fuzz -f docker-compose/fuzz/compose.yaml up --build --remove-orphans --abort-on-container-exit db cache backend graphql + update-backend-dependencies: @cd backend && poetry update diff --git a/backend/apps/api/rest/v0/__init__.py b/backend/apps/api/rest/v0/__init__.py index 93acf51a33..35f78e9c1f 100644 --- a/backend/apps/api/rest/v0/__init__.py +++ b/backend/apps/api/rest/v0/__init__.py @@ -4,6 +4,7 @@ from django.conf import settings from ninja import NinjaAPI, Swagger +from ninja.errors import ValidationError from ninja.pagination import RouterPaginated from ninja.throttling import AuthRateThrottle @@ -57,6 +58,28 @@ } ], } +elif settings.IS_E2E_ENVIRONMENT: + api_settings_customization = { + "auth": None, + "servers": [ + { + "description": "E2E", + "url": settings.SITE_URL, + } + ], + "throttle": [], + } +elif settings.IS_FUZZ_ENVIRONMENT: + api_settings_customization = { + "auth": None, + "servers": [ + { + "description": "Fuzz", + "url": settings.SITE_URL, + } + ], + "throttle": [], + } elif settings.IS_STAGING_ENVIRONMENT: api_settings_customization = { "servers": [ @@ -81,6 +104,16 @@ api = NinjaAPI(**{**api_settings, **api_settings_customization}) +@api.exception_handler(ValidationError) +def validation_exception_handler(request, exc): + """Handle validation exceptions.""" + return api.create_response( + request, + {"message": "Invalid request", "errors": exc.errors}, + status=400, + ) + + @api.get("/", include_in_schema=False) def api_root(request): """Handle API root endpoint requests.""" diff --git a/backend/apps/api/rest/v0/chapter.py b/backend/apps/api/rest/v0/chapter.py index be7d9290f1..28e8ebfdc6 100644 --- a/backend/apps/api/rest/v0/chapter.py +++ b/backend/apps/api/rest/v0/chapter.py @@ -11,7 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response -from apps.api.rest.v0.common import Leader, LocationFilter +from apps.api.rest.v0.common import Leader, LocationFilter, ValidationErrorSchema from apps.owasp.models.chapter import Chapter as ChapterModel router = RouterPaginated(tags=["Chapters"]) @@ -100,6 +100,7 @@ def list_chapters( description="Retrieve chapter details.", operation_id="get_chapter", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: ChapterError, HTTPStatus.OK: ChapterDetail, }, diff --git a/backend/apps/api/rest/v0/committee.py b/backend/apps/api/rest/v0/committee.py index 87eb0618cf..d14a9cbf0b 100644 --- a/backend/apps/api/rest/v0/committee.py +++ b/backend/apps/api/rest/v0/committee.py @@ -11,6 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response +from apps.api.rest.v0.common import ValidationErrorSchema from apps.owasp.models.committee import Committee as CommitteeModel router = RouterPaginated(tags=["Committees"]) @@ -70,6 +71,7 @@ def list_committees( description="Retrieve committee details.", operation_id="get_committee", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: CommitteeError, HTTPStatus.OK: CommitteeDetail, }, diff --git a/backend/apps/api/rest/v0/common.py b/backend/apps/api/rest/v0/common.py index 123207e086..55444ec6ea 100644 --- a/backend/apps/api/rest/v0/common.py +++ b/backend/apps/api/rest/v0/common.py @@ -10,6 +10,13 @@ class Leader(Schema): name: str +class ValidationErrorSchema(Schema): + """Schema for validation error.""" + + message: str + errors: list[dict] | dict | None = None + + class LocationFilter(FilterSchema): """Filter for Location.""" diff --git a/backend/apps/api/rest/v0/event.py b/backend/apps/api/rest/v0/event.py index 2672522de7..342d2d7304 100644 --- a/backend/apps/api/rest/v0/event.py +++ b/backend/apps/api/rest/v0/event.py @@ -1,6 +1,5 @@ """Event API.""" -from datetime import datetime from http import HTTPStatus from typing import Literal @@ -11,7 +10,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response -from apps.api.rest.v0.common import LocationFilter +from apps.api.rest.v0.common import LocationFilter, ValidationErrorSchema from apps.owasp.models.event import Event as EventModel router = RouterPaginated(tags=["Events"]) @@ -20,14 +19,24 @@ class EventBase(Schema): """Base schema for Event (used in list endpoints).""" - end_date: datetime | None = None + end_date: str | None = None key: str latitude: float | None = None longitude: float | None = None name: str - start_date: datetime + start_date: str url: str | None = None + @staticmethod + def resolve_end_date(event: EventModel) -> str | None: + """Resolve end date.""" + return event.end_date.isoformat() if event.end_date else None + + @staticmethod + def resolve_start_date(event: EventModel) -> str: + """Resolve start date.""" + return event.start_date.isoformat() + class Event(EventBase): """Schema for Event (minimal fields for list display).""" @@ -90,6 +99,7 @@ def list_events( description="Retrieve an event details.", operation_id="get_event", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: EventError, HTTPStatus.OK: EventDetail, }, diff --git a/backend/apps/api/rest/v0/issue.py b/backend/apps/api/rest/v0/issue.py index a249b2f039..1fdb615cf0 100644 --- a/backend/apps/api/rest/v0/issue.py +++ b/backend/apps/api/rest/v0/issue.py @@ -11,6 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response +from apps.api.rest.v0.common import ValidationErrorSchema from apps.github.models.generic_issue_model import GenericIssueModel from apps.github.models.issue import Issue as IssueModel @@ -97,6 +98,7 @@ def list_issues( description="Retrieve a specific GitHub issue by organization, repository, and issue number.", operation_id="get_issue", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: IssueError, HTTPStatus.OK: IssueDetail, }, diff --git a/backend/apps/api/rest/v0/member.py b/backend/apps/api/rest/v0/member.py index 249e74655d..ce0769d085 100644 --- a/backend/apps/api/rest/v0/member.py +++ b/backend/apps/api/rest/v0/member.py @@ -11,6 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response +from apps.api.rest.v0.common import ValidationErrorSchema from apps.github.models.user import User as UserModel router = RouterPaginated(tags=["Community"]) @@ -85,6 +86,7 @@ def list_members( description="Retrieve member details.", operation_id="get_member", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: MemberError, HTTPStatus.OK: MemberDetail, }, diff --git a/backend/apps/api/rest/v0/milestone.py b/backend/apps/api/rest/v0/milestone.py index 17ec54c197..c4761b6f0f 100644 --- a/backend/apps/api/rest/v0/milestone.py +++ b/backend/apps/api/rest/v0/milestone.py @@ -11,6 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response +from apps.api.rest.v0.common import ValidationErrorSchema from apps.github.models.generic_issue_model import GenericIssueModel from apps.github.models.milestone import Milestone as MilestoneModel @@ -101,6 +102,7 @@ def list_milestones( ), operation_id="get_milestone", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: MilestoneError, HTTPStatus.OK: MilestoneDetail, }, diff --git a/backend/apps/api/rest/v0/organization.py b/backend/apps/api/rest/v0/organization.py index b214307217..862274eab3 100644 --- a/backend/apps/api/rest/v0/organization.py +++ b/backend/apps/api/rest/v0/organization.py @@ -11,6 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response +from apps.api.rest.v0.common import ValidationErrorSchema from apps.github.models.organization import Organization as OrganizationModel router = RouterPaginated(tags=["Community"]) @@ -81,6 +82,7 @@ def list_organization( description="Retrieve project details.", operation_id="get_organization", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: OrganizationError, HTTPStatus.OK: OrganizationDetail, }, diff --git a/backend/apps/api/rest/v0/project.py b/backend/apps/api/rest/v0/project.py index 11d676321e..640530607a 100644 --- a/backend/apps/api/rest/v0/project.py +++ b/backend/apps/api/rest/v0/project.py @@ -11,7 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response -from apps.api.rest.v0.common import Leader +from apps.api.rest.v0.common import Leader, ValidationErrorSchema from apps.owasp.models.enums.project import ProjectLevel from apps.owasp.models.project import Project as ProjectModel @@ -96,6 +96,7 @@ def list_projects( description="Retrieve project details.", operation_id="get_project", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: ProjectError, HTTPStatus.OK: ProjectDetail, }, diff --git a/backend/apps/api/rest/v0/release.py b/backend/apps/api/rest/v0/release.py index 2d4c97d0bb..04073129e5 100644 --- a/backend/apps/api/rest/v0/release.py +++ b/backend/apps/api/rest/v0/release.py @@ -11,6 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response +from apps.api.rest.v0.common import ValidationErrorSchema from apps.github.models.release import Release as ReleaseModel router = RouterPaginated(tags=["Releases"]) @@ -98,6 +99,7 @@ def list_release( description="Retrieve a specific GitHub release by organization, repository, and tag name.", operation_id="get_release", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: ReleaseError, HTTPStatus.OK: ReleaseDetail, }, diff --git a/backend/apps/api/rest/v0/repository.py b/backend/apps/api/rest/v0/repository.py index 2e2214c23f..bbf652780f 100644 --- a/backend/apps/api/rest/v0/repository.py +++ b/backend/apps/api/rest/v0/repository.py @@ -11,6 +11,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response +from apps.api.rest.v0.common import ValidationErrorSchema from apps.github.models.repository import Repository as RepositoryModel router = RouterPaginated(tags=["Repositories"]) @@ -85,6 +86,7 @@ def list_repository( description="Retrieve a specific GitHub repository by organization and repository name.", operation_id="get_repository", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: RepositoryError, HTTPStatus.OK: RepositoryDetail, }, diff --git a/backend/apps/api/rest/v0/snapshot.py b/backend/apps/api/rest/v0/snapshot.py index 50ef169d6c..a26ee516c8 100644 --- a/backend/apps/api/rest/v0/snapshot.py +++ b/backend/apps/api/rest/v0/snapshot.py @@ -12,6 +12,7 @@ from apps.api.decorators.cache import cache_response from apps.api.rest.v0.chapter import Chapter +from apps.api.rest.v0.common import ValidationErrorSchema from apps.api.rest.v0.issue import Issue from apps.api.rest.v0.member import Member from apps.api.rest.v0.project import Project @@ -130,6 +131,7 @@ def list_snapshots( description="Retrieve snapshot details.", operation_id="get_snapshot", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: SnapshotError, HTTPStatus.OK: SnapshotDetail, }, diff --git a/backend/apps/api/rest/v0/sponsor.py b/backend/apps/api/rest/v0/sponsor.py index e00ab64612..4641e7639c 100644 --- a/backend/apps/api/rest/v0/sponsor.py +++ b/backend/apps/api/rest/v0/sponsor.py @@ -10,6 +10,7 @@ from ninja.responses import Response from apps.api.decorators.cache import cache_response +from apps.api.rest.v0.common import ValidationErrorSchema from apps.owasp.models.sponsor import Sponsor as SponsorModel router = RouterPaginated(tags=["Sponsors"]) @@ -88,6 +89,7 @@ def list_sponsors( description="Retrieve a sponsor details.", operation_id="get_sponsor", response={ + HTTPStatus.BAD_REQUEST: ValidationErrorSchema, HTTPStatus.NOT_FOUND: SponsorError, HTTPStatus.OK: SponsorDetail, }, diff --git a/backend/apps/common/management/commands/dump_data.py b/backend/apps/common/management/commands/dump_data.py new file mode 100644 index 0000000000..8c9631338c --- /dev/null +++ b/backend/apps/common/management/commands/dump_data.py @@ -0,0 +1,144 @@ +"""Dump masked data from the database into a compressed file.""" + +import contextlib +import os +from pathlib import Path +from subprocess import CalledProcessError, run + +from django.conf import settings +from django.core.management.base import BaseCommand, CommandError +from psycopg2 import OperationalError, ProgrammingError, connect, sql + +DEFAULT_DATABASE = settings.DATABASES["default"] +DB_HOST = DEFAULT_DATABASE.get("HOST", "localhost") +DB_PORT = str(DEFAULT_DATABASE.get("PORT", "5432")) +DB_USER = DEFAULT_DATABASE.get("USER", "") +DB_PASSWORD = DEFAULT_DATABASE.get("PASSWORD", "") +DB_NAME = DEFAULT_DATABASE.get("NAME", "") + + +class Command(BaseCommand): + help = "Create a dump of selected db tables." + + def add_arguments(self, parser): + parser.add_argument( + "--output", + default=str(Path(settings.BASE_DIR) / "data" / "nest.dump"), + help="Output dump path (default: data/nest.dump)", + ) + parser.add_argument( + "-t", + "--table", + action="append", + dest="tables", + default=[ + "public.owasp_*", + "public.github_*", + "public.slack_members", + "public.slack_workspaces", + "public.slack_conversations", + "public.slack_messages", + ], + help="Table pattern to include", + ) + + def handle(self, *args, **options): + output_path = Path(options["output"]).resolve() + output_path.parent.mkdir(parents=True, exist_ok=True) + tables = options["tables"] or [] + + env = os.environ.copy() + env["PGPASSWORD"] = DB_PASSWORD + + temp_db = f"temp_{DB_NAME}" + try: + self._execute_sql( + "postgres", + [ + sql.SQL("CREATE DATABASE {temp_db} TEMPLATE {DB_NAME};").format( + temp_db=sql.Identifier(temp_db), DB_NAME=sql.Identifier(DB_NAME) + ) + ], + ) + + self.stdout.write(self.style.SUCCESS(f"Created temporary DB: {temp_db}")) + + table_list = self._execute_sql(temp_db, [self._table_list_query()]) + self._execute_sql(temp_db, self._remove_emails([row[0] for row in table_list])) + self.stdout.write(self.style.SUCCESS("Removed emails from temporary DB")) + + dump_cmd = [ + "pg_dump", + "-h", + DB_HOST, + "-p", + DB_PORT, + "-U", + DB_USER, + "-d", + temp_db, + "--compress=9", + "--data-only", + "--no-owner", + "--no-privileges", + "--format=custom", + ] + dump_cmd += [f"--table={table}" for table in tables] + dump_cmd += ["-f", str(output_path)] + + run(dump_cmd, check=True, env=env) + self.stdout.write(self.style.SUCCESS(f"Created dump: {output_path}")) + except CalledProcessError as e: + message = f"Command failed: {e.cmd}" + raise CommandError(message) from e + finally: + try: + self._execute_sql( + "postgres", + [ + sql.SQL("DROP DATABASE IF EXISTS {temp_db};").format( + temp_db=sql.Identifier(temp_db) + ) + ], + ) + except (ProgrammingError, OperationalError): + self.stderr.write( + self.style.WARNING(f"Failed to drop temp DB {temp_db} (ignored).") + ) + + def _table_list_query(self) -> sql.Composable: + return sql.SQL(""" + SELECT table_name + FROM information_schema.columns + WHERE table_schema = 'public' AND column_name = 'email'; + """) + + def _remove_emails(self, tables: list[str]) -> list[sql.Composable]: + return [ + sql.SQL("UPDATE {table} SET email = '';").format(table=sql.Identifier(table)) + for table in tables + ] + + def _execute_sql( + self, + dbname: str, + sql_queries: list[sql.Composable], + ): + connection = connect( + dbname=dbname, + user=DB_USER, + password=DB_PASSWORD, + host=DB_HOST, + port=DB_PORT, + ) + connection.autocommit = True + + rows = [] + with connection.cursor() as cursor: + for sql_query in sql_queries: + cursor.execute(sql_query) + with contextlib.suppress(ProgrammingError): + rows.extend(cursor.fetchall()) + connection.close() + + return rows diff --git a/backend/apps/common/middlewares/__init__.py b/backend/apps/common/middlewares/__init__.py new file mode 100644 index 0000000000..a52b722e66 --- /dev/null +++ b/backend/apps/common/middlewares/__init__.py @@ -0,0 +1 @@ +"""OWASP Nest Common middlewares.""" diff --git a/backend/apps/common/middlewares/block_null_characters.py b/backend/apps/common/middlewares/block_null_characters.py new file mode 100644 index 0000000000..c9cc03ccb2 --- /dev/null +++ b/backend/apps/common/middlewares/block_null_characters.py @@ -0,0 +1,44 @@ +"""OWASP Nest middleware to block null characters in requests.""" + +import logging +from http import HTTPStatus + +from django.http import HttpRequest, JsonResponse + +logger = logging.getLogger(__name__) + +NULL_CHARACTER = "\x00" + + +class BlockNullCharactersMiddleware: + """BlockNullCharactersMiddleware to block requests containing null characters.""" + + def __init__(self, get_response): + """Initialize middleware with get_response callable.""" + self.get_response = get_response + + def __call__(self, request: HttpRequest): + """Process the request to block null characters.""" + if ( + NULL_CHARACTER in request.path + or NULL_CHARACTER in request.path_info + or any( + NULL_CHARACTER in value for values in request.GET.lists() for value in values[1] + ) + or any( + NULL_CHARACTER in value for values in request.POST.lists() for value in values[1] + ) + ): + message = ( + "Request contains null characters in URL or parameters which are not allowed." + ) + logger.warning(message) + return JsonResponse({"message": message, "errors": {}}, status=HTTPStatus.BAD_REQUEST) + + content_length = int(request.META.get("CONTENT_LENGTH", 0) or 0) + if content_length and (b"\x00" in request.body or b"\\u0000" in request.body): + message = "Request contains null characters in body which are not allowed." + logger.warning(message) + return JsonResponse({"message": message, "errors": {}}, status=HTTPStatus.BAD_REQUEST) + + return self.get_response(request) diff --git a/backend/apps/common/utils.py b/backend/apps/common/utils.py index ffb721bb96..7e434beb97 100644 --- a/backend/apps/common/utils.py +++ b/backend/apps/common/utils.py @@ -100,7 +100,7 @@ def get_user_ip_address(request: HttpRequest) -> str: str: The user's IP address. """ - if settings.IS_LOCAL_ENVIRONMENT: + if settings.IS_LOCAL_ENVIRONMENT or settings.IS_E2E_ENVIRONMENT: return settings.PUBLIC_IP_ADDRESS x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") diff --git a/backend/apps/github/api/internal/nodes/issue.py b/backend/apps/github/api/internal/nodes/issue.py index 0bc99c3557..0312c25cdf 100644 --- a/backend/apps/github/api/internal/nodes/issue.py +++ b/backend/apps/github/api/internal/nodes/issue.py @@ -22,51 +22,40 @@ class IssueNode(strawberry.relay.Node): """GitHub issue node.""" - @strawberry.field - def author(self) -> UserNode | None: - """Resolve author.""" - return self.author + assignees: list[UserNode] = strawberry_django.field() + author: UserNode | None = strawberry_django.field() + pull_requests: list[PullRequestNode] = strawberry_django.field() - @strawberry.field - def organization_name(self) -> str | None: + @strawberry_django.field(select_related=["repository__organization", "repository"]) + def organization_name(self, root: Issue) -> str | None: """Resolve organization name.""" return ( - self.repository.organization.login - if self.repository and self.repository.organization + root.repository.organization.login + if root.repository and root.repository.organization else None ) - @strawberry.field - def repository_name(self) -> str | None: + @strawberry_django.field(select_related=["repository"]) + def repository_name(self, root: Issue) -> str | None: """Resolve the repository name.""" - return self.repository.name if self.repository else None + return root.repository.name if root.repository else None - @strawberry.field - def assignees(self) -> list[UserNode]: - """Resolve assignees list.""" - return list(self.assignees.all()) - - @strawberry.field - def labels(self) -> list[str]: + @strawberry_django.field(prefetch_related=["labels"]) + def labels(self, root: Issue) -> list[str]: """Resolve label names for the issue.""" - return list(self.labels.values_list("name", flat=True)) + return [label.name for label in root.labels.all()] - @strawberry.field - def is_merged(self) -> bool: + @strawberry_django.field(prefetch_related=["pull_requests"]) + def is_merged(self, root: Issue) -> bool: """Return True if this issue has at least one merged pull request.""" - return self.pull_requests.filter(state="closed", merged_at__isnull=False).exists() + return root.pull_requests.filter(state="closed", merged_at__isnull=False).exists() - @strawberry.field - def interested_users(self) -> list[UserNode]: + @strawberry_django.field(prefetch_related=["participant_interests__user"]) + def interested_users(self, root: Issue) -> list[UserNode]: """Return all users who have expressed interest in this issue.""" return [ interest.user - for interest in self.participant_interests.select_related("user").order_by( + for interest in root.participant_interests.select_related("user").order_by( "user__login" ) ] - - @strawberry.field - def pull_requests(self) -> list[PullRequestNode]: - """Return all pull requests linked to this issue.""" - return list(self.pull_requests.select_related("author", "repository").all()) diff --git a/backend/apps/github/api/internal/nodes/milestone.py b/backend/apps/github/api/internal/nodes/milestone.py index 17b5d67f2c..17d7db1159 100644 --- a/backend/apps/github/api/internal/nodes/milestone.py +++ b/backend/apps/github/api/internal/nodes/milestone.py @@ -22,29 +22,27 @@ class MilestoneNode(strawberry.relay.Node): """Github Milestone Node.""" - @strawberry.field - def author(self) -> UserNode | None: - """Resolve author.""" - return self.author + author: UserNode | None = strawberry_django.field() - @strawberry.field - def organization_name(self) -> str | None: + @strawberry_django.field(select_related=["repository__organization"]) + def organization_name(self, root: Milestone) -> str | None: """Resolve organization name.""" return ( - self.repository.organization.login - if self.repository and self.repository.organization + root.repository.organization.login + if root.repository and root.repository.organization else None ) - @strawberry.field - def progress(self) -> float: + @strawberry_django.field + def progress(self, root: Milestone) -> float: """Resolve milestone progress.""" - total_issues_count = self.closed_issues_count + self.open_issues_count - if not total_issues_count: - return 0.0 - return round((self.closed_issues_count / total_issues_count) * 100, 2) + return ( + round((root.closed_issues_count / total_issues_count) * 100, 2) + if (total_issues_count := root.closed_issues_count + root.open_issues_count) + else 0.0 + ) - @strawberry.field - def repository_name(self) -> str | None: + @strawberry_django.field(select_related=["repository"]) + def repository_name(self, root: Milestone) -> str | None: """Resolve repository name.""" - return self.repository.name if self.repository else None + return root.repository.name if root.repository else None diff --git a/backend/apps/github/api/internal/nodes/organization.py b/backend/apps/github/api/internal/nodes/organization.py index a249846bc7..08b53e7ad1 100644 --- a/backend/apps/github/api/internal/nodes/organization.py +++ b/backend/apps/github/api/internal/nodes/organization.py @@ -39,10 +39,10 @@ class OrganizationStatsNode: class OrganizationNode(strawberry.relay.Node): """GitHub organization node.""" - @strawberry.field - def stats(self) -> OrganizationStatsNode: + @strawberry_django.field + def stats(self, root: Organization) -> OrganizationStatsNode: """Resolve organization stats.""" - repositories = Repository.objects.filter(organization=self) + repositories = Repository.objects.filter(organization=root) total_repositories = repositories.count() aggregated_stats = repositories.aggregate( @@ -70,7 +70,7 @@ def stats(self) -> OrganizationStatsNode: total_issues=total_issues, ) - @strawberry.field - def url(self) -> str: + @strawberry_django.field + def url(self, root: Organization) -> str: """Resolve organization URL.""" - return self.url + return root.url diff --git a/backend/apps/github/api/internal/nodes/pull_request.py b/backend/apps/github/api/internal/nodes/pull_request.py index 7cd117e9fe..4e42bea9af 100644 --- a/backend/apps/github/api/internal/nodes/pull_request.py +++ b/backend/apps/github/api/internal/nodes/pull_request.py @@ -19,26 +19,23 @@ class PullRequestNode(strawberry.relay.Node): """GitHub pull request node.""" - @strawberry.field - def author(self) -> UserNode | None: - """Resolve author.""" - return self.author + author: UserNode | None = strawberry_django.field() - @strawberry.field - def organization_name(self) -> str | None: + @strawberry_django.field(select_related=["repository__organization"]) + def organization_name(self, root: PullRequest) -> str | None: """Resolve organization name.""" return ( - self.repository.organization.login - if self.repository and self.repository.organization + root.repository.organization.login + if root.repository and root.repository.organization else None ) - @strawberry.field - def repository_name(self) -> str | None: + @strawberry_django.field(select_related=["repository"]) + def repository_name(self, root: PullRequest) -> str | None: """Resolve repository name.""" - return self.repository.name if self.repository else None + return root.repository.name if root.repository else None - @strawberry.field - def url(self) -> str: + @strawberry_django.field + def url(self, root: PullRequest) -> str: """Resolve URL.""" - return str(self.url) if self.url else "" + return root.url diff --git a/backend/apps/github/api/internal/nodes/release.py b/backend/apps/github/api/internal/nodes/release.py index 0bf8e78cdf..36170bcddb 100644 --- a/backend/apps/github/api/internal/nodes/release.py +++ b/backend/apps/github/api/internal/nodes/release.py @@ -20,35 +20,34 @@ class ReleaseNode(strawberry.relay.Node): """GitHub release node.""" - @strawberry.field - def author(self) -> UserNode | None: - """Resolve author.""" - return self.author + author: UserNode | None = strawberry_django.field() - @strawberry.field - def organization_name(self) -> str | None: + @strawberry_django.field(select_related=["repository__organization"]) + def organization_name(self, root: Release) -> str | None: """Resolve organization name.""" return ( - self.repository.organization.login - if self.repository and self.repository.organization + root.repository.organization.login + if root.repository and root.repository.organization else None ) - @strawberry.field - def project_name(self) -> str | None: + @strawberry_django.field( + select_related=["repository"], prefetch_related=["repository__project_set"] + ) + def project_name(self, root: Release) -> str | None: """Resolve project name.""" return ( - self.repository.project.name.lstrip(OWASP_ORGANIZATION_NAME) - if self.repository and self.repository.project + root.repository.project.name.lstrip(OWASP_ORGANIZATION_NAME) + if root.repository and root.repository.project else None ) - @strawberry.field - def repository_name(self) -> str | None: + @strawberry_django.field(select_related=["repository"]) + def repository_name(self, root: Release) -> str | None: """Resolve repository name.""" - return self.repository.name if self.repository else None + return root.repository.name if root.repository else None - @strawberry.field - def url(self) -> str: + @strawberry_django.field + def url(self, root: Release) -> str: """Resolve URL.""" - return self.url + return root.url diff --git a/backend/apps/github/api/internal/nodes/repository.py b/backend/apps/github/api/internal/nodes/repository.py index e0791fe4c4..6a8c687d01 100644 --- a/backend/apps/github/api/internal/nodes/repository.py +++ b/backend/apps/github/api/internal/nodes/repository.py @@ -41,61 +41,53 @@ class RepositoryNode(strawberry.relay.Node): """Repository node.""" - @strawberry.field - def issues(self) -> list[IssueNode]: + organization: OrganizationNode | None = strawberry_django.field() + + @strawberry_django.field(prefetch_related=["issues"]) + def issues(self, root: Repository) -> list[IssueNode]: """Resolve recent issues.""" # TODO(arkid15r): rename this to recent_issues. - return self.issues.select_related("author").order_by("-created_at")[:RECENT_ISSUES_LIMIT] + return root.issues.order_by("-created_at")[:RECENT_ISSUES_LIMIT] - @strawberry.field - def languages(self) -> list[str]: + @strawberry_django.field + def languages(self, root: Repository) -> list[str]: """Resolve languages.""" - return list(self.languages.keys()) + return list(root.languages.keys()) - @strawberry.field - def latest_release(self) -> str: + @strawberry_django.field + def latest_release(self, root: Repository) -> str | None: """Resolve latest release.""" - return self.latest_release - - @strawberry.field - def organization(self) -> OrganizationNode | None: - """Resolve organization.""" - return self.organization - - @strawberry.field - def owner_key(self) -> str: - """Resolve owner key.""" - return self.owner_key + return root.latest_release - @strawberry.field + @strawberry_django.field(prefetch_related=["project_set"]) def project( - self, + self, root: Repository ) -> Annotated["ProjectNode", strawberry.lazy("apps.owasp.api.internal.nodes.project")] | None: """Resolve project.""" - return self.project + return root.project - @strawberry.field - def recent_milestones(self, limit: int = 5) -> list[MilestoneNode]: + @strawberry_django.field(prefetch_related=["milestones"]) + def recent_milestones(self, root: Repository, limit: int = 5) -> list[MilestoneNode]: """Resolve recent milestones.""" - return self.recent_milestones.select_related("repository").order_by("-created_at")[:limit] + return root.recent_milestones.order_by("-created_at")[:limit] - @strawberry.field - def releases(self) -> list[ReleaseNode]: + @strawberry_django.field(prefetch_related=["releases"]) + def releases(self, root: Repository) -> list[ReleaseNode]: """Resolve recent releases.""" # TODO(arkid15r): rename this to recent_releases. - return self.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] + return root.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] - @strawberry.field - def top_contributors(self) -> list[RepositoryContributorNode]: + @strawberry_django.field + def top_contributors(self, root: Repository) -> list[RepositoryContributorNode]: """Resolve top contributors.""" - return self.idx_top_contributors + return [RepositoryContributorNode(**tc) for tc in root.idx_top_contributors] - @strawberry.field - def topics(self) -> list[str]: + @strawberry_django.field + def topics(self, root: Repository) -> list[str]: """Resolve topics.""" - return self.topics + return root.topics - @strawberry.field - def url(self) -> str: + @strawberry_django.field + def url(self, root: Repository) -> str: """Resolve URL.""" - return self.url + return root.url diff --git a/backend/apps/github/api/internal/nodes/repository_contributor.py b/backend/apps/github/api/internal/nodes/repository_contributor.py index 6a43fb976f..21c9132ad5 100644 --- a/backend/apps/github/api/internal/nodes/repository_contributor.py +++ b/backend/apps/github/api/internal/nodes/repository_contributor.py @@ -12,5 +12,5 @@ class RepositoryContributorNode: id: strawberry.ID login: str name: str - project_key: str - project_name: str + project_key: str | None = None + project_name: str | None = None diff --git a/backend/apps/github/api/internal/nodes/user.py b/backend/apps/github/api/internal/nodes/user.py index c305d7764e..e7f635cbfc 100644 --- a/backend/apps/github/api/internal/nodes/user.py +++ b/backend/apps/github/api/internal/nodes/user.py @@ -1,6 +1,5 @@ """GitHub user GraphQL node.""" -import strawberry import strawberry_django from apps.github.models.user import User @@ -28,21 +27,19 @@ class UserNode: """GitHub user node.""" - @strawberry.field - def badge_count(self) -> int: + @strawberry_django.field(prefetch_related=["user_badges"]) + def badge_count(self, root: User) -> int: """Resolve badge count.""" - return self.user_badges.filter(is_active=True).count() + return root.user_badges.filter(is_active=True).count() - @strawberry.field - def badges(self) -> list[BadgeNode]: + @strawberry_django.field(prefetch_related=["user_badges__badge"]) + def badges(self, root: User) -> list[BadgeNode]: """Return user badges.""" user_badges = ( - self.user_badges.filter( + root.user_badges.filter( is_active=True, ) - .select_related( - "badge", - ) + .select_related("badge") .order_by( "badge__weight", "badge__name", @@ -50,62 +47,64 @@ def badges(self) -> list[BadgeNode]: ) return [user_badge.badge for user_badge in user_badges] - @strawberry.field - def created_at(self) -> float: + @strawberry_django.field + def created_at(self, root: User) -> float: """Resolve created at.""" - return self.idx_created_at + return root.idx_created_at - @strawberry.field - def first_owasp_contribution_at(self) -> float | None: + @strawberry_django.field(select_related=["owasp_profile"]) + def first_owasp_contribution_at(self, root: User) -> float | None: """Resolve first OWASP contribution date.""" - if hasattr(self, "owasp_profile") and self.owasp_profile.first_contribution_at: - return self.owasp_profile.first_contribution_at.timestamp() - return None + return ( + root.owasp_profile.first_contribution_at.timestamp() + if hasattr(root, "owasp_profile") and root.owasp_profile.first_contribution_at + else None + ) - @strawberry.field - def is_owasp_board_member(self) -> bool: + @strawberry_django.field(select_related=["owasp_profile"]) + def is_owasp_board_member(self, root: User) -> bool: """Resolve if member is currently on OWASP Board of Directors.""" - if hasattr(self, "owasp_profile"): - return self.owasp_profile.is_owasp_board_member - return False + return ( + root.owasp_profile.is_owasp_board_member if hasattr(root, "owasp_profile") else False + ) - @strawberry.field - def is_former_owasp_staff(self) -> bool: + @strawberry_django.field(select_related=["owasp_profile"]) + def is_former_owasp_staff(self, root: User) -> bool: """Resolve if member is a former OWASP staff member.""" - if hasattr(self, "owasp_profile"): - return self.owasp_profile.is_former_owasp_staff - return False + return ( + root.owasp_profile.is_former_owasp_staff if hasattr(root, "owasp_profile") else False + ) - @strawberry.field - def is_gsoc_mentor(self) -> bool: + @strawberry_django.field(select_related=["owasp_profile"]) + def is_gsoc_mentor(self, root: User) -> bool: """Resolve if member is a Google Summer of Code mentor.""" - if hasattr(self, "owasp_profile"): - return self.owasp_profile.is_gsoc_mentor - return False + return root.owasp_profile.is_gsoc_mentor if hasattr(root, "owasp_profile") else False - @strawberry.field - def issues_count(self) -> int: + @strawberry_django.field + def issues_count(self, root: User) -> int: """Resolve issues count.""" - return self.idx_issues_count + return root.idx_issues_count - @strawberry.field - def linkedin_page_id(self) -> str: + @strawberry_django.field(select_related=["owasp_profile"]) + def linkedin_page_id(self, root: User) -> str: """Resolve LinkedIn page ID.""" - if hasattr(self, "owasp_profile") and self.owasp_profile.linkedin_page_id: - return self.owasp_profile.linkedin_page_id - return "" + return ( + root.owasp_profile.linkedin_page_id + if hasattr(root, "owasp_profile") and root.owasp_profile.linkedin_page_id + else "" + ) - @strawberry.field - def releases_count(self) -> int: + @strawberry_django.field + def releases_count(self, root: User) -> int: """Resolve releases count.""" - return self.idx_releases_count + return root.idx_releases_count - @strawberry.field - def updated_at(self) -> float: + @strawberry_django.field + def updated_at(self, root: User) -> float: """Resolve updated at.""" - return self.idx_updated_at + return root.idx_updated_at - @strawberry.field - def url(self) -> str: + @strawberry_django.field + def url(self, root: User) -> str: """Resolve URL.""" - return self.url + return root.url diff --git a/backend/apps/github/api/internal/queries/issue.py b/backend/apps/github/api/internal/queries/issue.py index 87cb288401..c1c077c2b3 100644 --- a/backend/apps/github/api/internal/queries/issue.py +++ b/backend/apps/github/api/internal/queries/issue.py @@ -1,17 +1,21 @@ """GraphQL queries for handling GitHub issues.""" import strawberry -from django.db.models import OuterRef, Subquery +import strawberry_django +from django.db.models import F, Window +from django.db.models.functions import Rank from apps.github.api.internal.nodes.issue import IssueNode from apps.github.models.issue import Issue +MAX_LIMIT = 1000 + @strawberry.type class IssueQuery: """GraphQL query class for retrieving GitHub issues.""" - @strawberry.field + @strawberry_django.field def recent_issues( self, *, @@ -23,7 +27,7 @@ def recent_issues( """Resolve recent issues with optional filtering. Args: - distinct (bool): Whether to return unique issues per author and repository. + distinct (bool): Whether to return unique issues per author. limit (int): Maximum number of issues to return. login (str, optional): Filter issues by a specific author's login. organization (str, optional): Filter issues by a specific organization's login. @@ -32,34 +36,29 @@ def recent_issues( list[IssueNode]: List of issue nodes. """ - queryset = Issue.objects.select_related( - "author", - "repository", - "repository__organization", - ).order_by( + queryset = Issue.objects.order_by( "-created_at", ) + filters = {} if login: - queryset = queryset.filter( - author__login=login, - ) - + filters["author__login"] = login if organization: - queryset = queryset.filter( - repository__organization__login=organization, - ) + filters["repository__organization__login"] = organization + + queryset = queryset.filter(**filters) if distinct: - latest_issue_per_author = ( - queryset.filter(author_id=OuterRef("author_id")) + queryset = ( + queryset.annotate( + rank=Window( + expression=Rank(), + partition_by=[F("author_id")], + order_by=F("created_at").desc(), + ) + ) + .filter(rank=1) .order_by("-created_at") - .values("id")[:1] - ) - queryset = queryset.filter( - id__in=Subquery(latest_issue_per_author), - ).order_by( - "-created_at", ) - return queryset[:limit] + return queryset[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else [] diff --git a/backend/apps/github/api/internal/queries/milestone.py b/backend/apps/github/api/internal/queries/milestone.py index 8c95ff55cf..bb87fab3a4 100644 --- a/backend/apps/github/api/internal/queries/milestone.py +++ b/backend/apps/github/api/internal/queries/milestone.py @@ -1,18 +1,31 @@ """Github Milestone Queries.""" +import enum + import strawberry -from django.core.exceptions import ValidationError +import strawberry_django from django.db.models import OuterRef, Subquery from apps.github.api.internal.nodes.milestone import MilestoneNode +from apps.github.models.generic_issue_model import GenericIssueModel from apps.github.models.milestone import Milestone +MAX_LIMIT = 1000 + + +@strawberry.enum +class MilestoneStateEnum(str, enum.Enum): + """Milestone state filter options.""" + + CLOSED = GenericIssueModel.State.CLOSED.value + OPEN = GenericIssueModel.State.OPEN.value + @strawberry.type class MilestoneQuery: """Github Milestone Queries.""" - @strawberry.field + @strawberry_django.field def recent_milestones( self, *, @@ -20,7 +33,7 @@ def recent_milestones( limit: int = 5, login: str | None = None, organization: str | None = None, - state: str = "open", + state: MilestoneStateEnum | None = None, ) -> list[MilestoneNode]: """Resolve milestones. @@ -29,32 +42,20 @@ def recent_milestones( limit (int): The maximum number of milestones to return. login (str, optional): The GitHub username to filter milestones. organization (str, optional): The GitHub organization to filter milestones. - state (str, optional): The state of the milestones to return. + state (MilestoneStateEnum, optional): The state filter. Returns all if not provided. Returns: list[MilestoneNode]: A list of milestones. """ - match state.lower(): - case "open": + match state: + case MilestoneStateEnum.OPEN: milestones = Milestone.open_milestones.all() - case "closed": + case MilestoneStateEnum.CLOSED: milestones = Milestone.closed_milestones.all() - case "all": - milestones = Milestone.objects.all() case _: - message = f"Invalid state: {state}. Valid states are 'open', 'closed', or 'all'." - raise ValidationError(message) - - milestones = milestones.select_related( - "author", - "repository", - "repository__organization", - ).prefetch_related( - "issues", - "labels", - "pull_requests", - ) + milestones = Milestone.objects.all() + if login: milestones = milestones.filter( author__login=login, @@ -75,6 +76,8 @@ def recent_milestones( id__in=Subquery(latest_milestone_per_author), ) - return milestones.order_by( - "-created_at", - )[:limit] + return ( + milestones.order_by("-created_at")[:limit] + if (limit := min(limit, MAX_LIMIT)) > 0 + else [] + ) diff --git a/backend/apps/github/api/internal/queries/pull_request.py b/backend/apps/github/api/internal/queries/pull_request.py index 4649a55a91..d001331003 100644 --- a/backend/apps/github/api/internal/queries/pull_request.py +++ b/backend/apps/github/api/internal/queries/pull_request.py @@ -1,18 +1,22 @@ """Github pull requests GraphQL queries.""" import strawberry -from django.db.models import OuterRef, Subquery +import strawberry_django +from django.db.models import F, Window +from django.db.models.functions import Rank from apps.github.api.internal.nodes.pull_request import PullRequestNode from apps.github.models.pull_request import PullRequest from apps.owasp.models.project import Project +MAX_LIMIT = 1000 + @strawberry.type class PullRequestQuery: """Pull request queries.""" - @strawberry.field + @strawberry_django.field def recent_pull_requests( self, *, @@ -26,7 +30,7 @@ def recent_pull_requests( """Resolve recent pull requests. Args: - distinct (bool): Whether to return unique pull requests per author and repository. + distinct (bool): Whether to return unique pull requests per author. limit (int): Maximum number of pull requests to return. login (str, optional): Filter pull requests by a specific author's login. organization (str, optional): Filter pull requests by a specific organization's login. @@ -38,52 +42,44 @@ def recent_pull_requests( filtered list of pull requests. """ - queryset = ( - PullRequest.objects.select_related( - "author", - "repository", - "repository__organization", - ) - .exclude( - author__is_bot=True, - ) - .order_by( - "-created_at", - ) + queryset = PullRequest.objects.exclude( + author__is_bot=True, + ).order_by( + "-created_at", ) + filters = {} if login: - queryset = queryset.filter(author__login=login) + filters["author__login"] = login if organization: - queryset = queryset.filter( - repository__organization__login=organization, - ) + filters["repository__organization__login"] = organization + + queryset = queryset.filter(**filters) if project: - queryset = queryset.filter( - repository_id__in=Project.objects.filter(key__iexact=f"www-project-{project}") - .first() - .repositories.values_list("id", flat=True) - ) + project_instance = Project.objects.filter(key__iexact=f"www-project-{project}").first() + if project_instance: + queryset = queryset.filter( + repository_id__in=project_instance.repositories.values_list("id", flat=True) + ) + else: + queryset = queryset.none() if repository: queryset = queryset.filter(repository__key__iexact=repository) if distinct: - latest_pull_request_per_author = ( - queryset.filter( - author_id=OuterRef("author_id"), + queryset = ( + queryset.annotate( + rank=Window( + expression=Rank(), + partition_by=[F("author_id")], + order_by=F("created_at").desc(), + ) ) - .order_by( - "-created_at", - ) - .values("id")[:1] - ) - queryset = queryset.filter( - id__in=Subquery(latest_pull_request_per_author), - ).order_by( - "-created_at", + .filter(rank=1) + .order_by("-created_at") ) - return queryset[:limit] + return queryset[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else [] diff --git a/backend/apps/github/api/internal/queries/release.py b/backend/apps/github/api/internal/queries/release.py index f9e3a60b6c..928d78a9a8 100644 --- a/backend/apps/github/api/internal/queries/release.py +++ b/backend/apps/github/api/internal/queries/release.py @@ -1,17 +1,21 @@ """GraphQL queries for handling OWASP releases.""" import strawberry -from django.db.models import OuterRef, Subquery +import strawberry_django +from django.db.models import F, Window +from django.db.models.functions import Rank from apps.github.api.internal.nodes.release import ReleaseNode from apps.github.models.release import Release +MAX_LIMIT = 1000 + @strawberry.type class ReleaseQuery: """GraphQL query class for retrieving recent GitHub releases.""" - @strawberry.field + @strawberry_django.field def recent_releases( self, *, @@ -23,7 +27,7 @@ def recent_releases( """Resolve recent releases with optional distinct filtering. Args: - distinct (bool): Whether to return unique releases per author and repository. + distinct (bool): Whether to return unique releases per author. limit (int): Maximum number of releases to return. login (str, optional): Filter releases by a specific author's login. organization (str, optional): Filter releases by a specific organization's login. @@ -39,11 +43,7 @@ def recent_releases( ).order_by("-published_at") if login: - queryset = queryset.select_related( - "author", - "repository", - "repository__organization", - ).filter( + queryset = queryset.filter( author__login=login, ) @@ -53,15 +53,16 @@ def recent_releases( ) if distinct: - latest_release_per_author = ( - queryset.filter(author_id=OuterRef("author_id")) + queryset = ( + queryset.annotate( + rank=Window( + expression=Rank(), + partition_by=[F("author_id")], + order_by=F("published_at").desc(), + ) + ) + .filter(rank=1) .order_by("-published_at") - .values("id")[:1] - ) - queryset = queryset.filter( - id__in=Subquery(latest_release_per_author), - ).order_by( - "-published_at", ) - return queryset[:limit] + return queryset[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else [] diff --git a/backend/apps/github/api/internal/queries/repository.py b/backend/apps/github/api/internal/queries/repository.py index b7bf1a59c7..70a0a033b8 100644 --- a/backend/apps/github/api/internal/queries/repository.py +++ b/backend/apps/github/api/internal/queries/repository.py @@ -1,16 +1,19 @@ """OWASP repository GraphQL queries.""" import strawberry +import strawberry_django from apps.github.api.internal.nodes.repository import RepositoryNode from apps.github.models.repository import Repository +MAX_LIMIT = 1000 + @strawberry.type class RepositoryQuery: """Repository queries.""" - @strawberry.field + @strawberry_django.field def repository( self, organization_key: str, @@ -34,7 +37,7 @@ def repository( except Repository.DoesNotExist: return None - @strawberry.field + @strawberry_django.field def repositories( self, organization: str, @@ -52,11 +55,11 @@ def repositories( """ return ( - Repository.objects.select_related( - "organization", - ) - .filter( - organization__login__iexact=organization, + ( + Repository.objects.filter( + organization__login__iexact=organization, + ).order_by("-stars_count")[:limit] ) - .order_by("-stars_count")[:limit] + if (limit := min(limit, MAX_LIMIT)) > 0 + else [] ) diff --git a/backend/apps/github/api/internal/queries/repository_contributor.py b/backend/apps/github/api/internal/queries/repository_contributor.py index fd2716c8ba..9ae9c8f0b5 100644 --- a/backend/apps/github/api/internal/queries/repository_contributor.py +++ b/backend/apps/github/api/internal/queries/repository_contributor.py @@ -1,16 +1,19 @@ """OWASP repository contributor GraphQL queries.""" import strawberry +import strawberry_django from apps.github.api.internal.nodes.repository_contributor import RepositoryContributorNode from apps.github.models.repository_contributor import RepositoryContributor +MAX_LIMIT = 100 + @strawberry.type class RepositoryContributorQuery: """Repository contributor queries.""" - @strawberry.field + @strawberry_django.field def top_contributors( self, *, @@ -39,6 +42,9 @@ def top_contributors( list: List of top contributors with their details. """ + if (limit := min(limit, MAX_LIMIT)) <= 0: + return [] + top_contributors = RepositoryContributor.get_top_contributors( chapter=chapter, committee=committee, diff --git a/backend/apps/github/api/internal/queries/user.py b/backend/apps/github/api/internal/queries/user.py index 199c33aa90..d99df0de43 100644 --- a/backend/apps/github/api/internal/queries/user.py +++ b/backend/apps/github/api/internal/queries/user.py @@ -1,6 +1,7 @@ """OWASP user GraphQL queries.""" import strawberry +import strawberry_django from apps.github.api.internal.nodes.repository import RepositoryNode from apps.github.api.internal.nodes.user import UserNode @@ -12,7 +13,7 @@ class UserQuery: """User queries.""" - @strawberry.field + @strawberry_django.field def top_contributed_repositories( self, login: str, @@ -36,7 +37,7 @@ def top_contributed_repositories( .order_by("-contributions_count") ] - @strawberry.field + @strawberry_django.field def user( self, login: str, diff --git a/backend/apps/github/migrations/0041_milestone_github_milestone_created_at_and_more.py b/backend/apps/github/migrations/0041_milestone_github_milestone_created_at_and_more.py new file mode 100644 index 0000000000..8837e1a27e --- /dev/null +++ b/backend/apps/github/migrations/0041_milestone_github_milestone_created_at_and_more.py @@ -0,0 +1,20 @@ +# Generated by Django 6.0 on 2026-01-11 18:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0040_merge_20251117_0136"), + ] + + operations = [ + migrations.AddIndex( + model_name="milestone", + index=models.Index(fields=["-created_at"], name="github_milestone_created_at"), + ), + migrations.AddIndex( + model_name="milestone", + index=models.Index(fields=["-updated_at"], name="github_milestone_updated_at"), + ), + ] diff --git a/backend/apps/github/models/milestone.py b/backend/apps/github/models/milestone.py index 77ffa1e3c4..8d88552e33 100644 --- a/backend/apps/github/models/milestone.py +++ b/backend/apps/github/models/milestone.py @@ -19,6 +19,11 @@ class Meta: verbose_name_plural = "Milestones" ordering = ["-updated_at", "-state"] + indexes = [ + models.Index(fields=["-created_at"], name="github_milestone_created_at"), + models.Index(fields=["-updated_at"], name="github_milestone_updated_at"), + ] + open_issues_count = models.PositiveIntegerField(default=0) closed_issues_count = models.PositiveIntegerField(default=0) due_on = models.DateTimeField(blank=True, null=True) diff --git a/backend/apps/github/models/organization.py b/backend/apps/github/models/organization.py index b1fadf90d6..a062cf6061 100644 --- a/backend/apps/github/models/organization.py +++ b/backend/apps/github/models/organization.py @@ -54,7 +54,9 @@ def related_projects(self) -> QuerySet: """Return organization related projects.""" return ( apps.get_model("owasp", "Project") # Dynamic import. - .objects.filter( + .objects.select_related("owasp_repository") + .prefetch_related("organizations", "owners", "repositories") + .filter( repositories__in=self.repositories.all(), ) .distinct() diff --git a/backend/apps/github/models/repository.py b/backend/apps/github/models/repository.py index 23bf79fef1..1fad7e052f 100644 --- a/backend/apps/github/models/repository.py +++ b/backend/apps/github/models/repository.py @@ -10,7 +10,6 @@ from apps.common.models import TimestampedModel from apps.github.constants import OWASP_LOGIN from apps.github.models.common import NodeModel -from apps.github.models.milestone import Milestone from apps.github.models.mixins import RepositoryIndexMixin from apps.github.utils import ( check_funding_policy_compliance, @@ -174,9 +173,7 @@ def published_releases(self): @property def recent_milestones(self): """Repository recent milestones.""" - return Milestone.objects.filter( - repository=self, - ).order_by("-created_at") + return self.milestones.order_by("-created_at") if self.pk else [] @property def top_languages(self) -> list[str]: diff --git a/backend/apps/github/models/repository_contributor.py b/backend/apps/github/models/repository_contributor.py index 982eb2c190..fd920a8ff0 100644 --- a/backend/apps/github/models/repository_contributor.py +++ b/backend/apps/github/models/repository_contributor.py @@ -179,21 +179,26 @@ def get_top_contributors( # Aggregate total contributions for users. top_contributors = ( - queryset.values( - "user__avatar_url", - "user__login", - "user__name", + ( + queryset.values( + "user__avatar_url", + "user__login", + "user__name", + ) + .annotate( + total_contributions=Sum("contributions_count"), + ) + .order_by("-total_contributions")[:limit] ) - .annotate( - total_contributions=Sum("contributions_count"), - ) - .order_by("-total_contributions")[:limit] + if limit > 0 + else [] ) return [ { "avatar_url": tc["user__avatar_url"], "contributions_count": tc["total_contributions"], + "id": tc["user__login"], "login": tc["user__login"], "name": tc["user__name"], } diff --git a/backend/apps/mentorship/api/internal/queries/mentorship.py b/backend/apps/mentorship/api/internal/queries/mentorship.py index cab13ed8d5..a0ae2df5eb 100644 --- a/backend/apps/mentorship/api/internal/queries/mentorship.py +++ b/backend/apps/mentorship/api/internal/queries/mentorship.py @@ -2,10 +2,10 @@ from __future__ import annotations +import logging from typing import TYPE_CHECKING, cast import strawberry -from django.core.exceptions import ObjectDoesNotExist from django.db.models import Prefetch from apps.github.api.internal.nodes.issue import IssueNode @@ -20,6 +20,8 @@ if TYPE_CHECKING: from apps.github.api.internal.nodes.issue import IssueNode +logger = logging.getLogger(__name__) + @strawberry.type class UserRolesResult: @@ -48,7 +50,9 @@ def is_mentor(self, login: str) -> bool: return Mentor.objects.filter(github_user=github_user).exists() @strawberry.field - def get_mentee_details(self, program_key: str, module_key: str, mentee_key: str) -> MenteeNode: + def get_mentee_details( + self, program_key: str, module_key: str, mentee_key: str + ) -> MenteeNode | None: """Get detailed information about a mentee in a specific module.""" try: module = Module.objects.only("id").get(key=module_key, program__key=program_key) @@ -64,7 +68,8 @@ def get_mentee_details(self, program_key: str, module_key: str, mentee_key: str) if not is_enrolled: message = f"Mentee {mentee_key} is not enrolled in module {module_key}" - raise ObjectDoesNotExist(message) + logger.warning(message) + return None return MenteeNode( id=cast("strawberry.ID", str(mentee.id)), @@ -79,7 +84,8 @@ def get_mentee_details(self, program_key: str, module_key: str, mentee_key: str) except (Module.DoesNotExist, GithubUser.DoesNotExist, Mentee.DoesNotExist) as e: message = f"Mentee details not found: {e}" - raise ObjectDoesNotExist(message) from e + logger.warning(message) + return None @strawberry.field def get_mentee_module_issues( @@ -101,7 +107,8 @@ def get_mentee_module_issues( if not is_enrolled: message = f"Mentee {mentee_key} is not enrolled in module {module_key}" - raise ObjectDoesNotExist(message) + logger.warning(message) + return [] issues_qs = ( module.issues.filter(assignees=github_user) @@ -121,4 +128,5 @@ def get_mentee_module_issues( except (Module.DoesNotExist, GithubUser.DoesNotExist, Mentee.DoesNotExist) as e: message = f"Mentee issues not found: {e}" - raise ObjectDoesNotExist(message) from e + logger.warning(message) + return [] diff --git a/backend/apps/mentorship/api/internal/queries/module.py b/backend/apps/mentorship/api/internal/queries/module.py index 8fec753752..aed931a37f 100644 --- a/backend/apps/mentorship/api/internal/queries/module.py +++ b/backend/apps/mentorship/api/internal/queries/module.py @@ -3,7 +3,6 @@ import logging import strawberry -from django.core.exceptions import ObjectDoesNotExist from apps.mentorship.api.internal.nodes.module import ModuleNode from apps.mentorship.models import Module @@ -36,7 +35,7 @@ def get_project_modules(self, project_key: str) -> list[ModuleNode]: ) @strawberry.field - def get_module(self, module_key: str, program_key: str) -> ModuleNode: + def get_module(self, module_key: str, program_key: str) -> ModuleNode | None: """Get a single module by its key within a specific program.""" try: return ( @@ -44,7 +43,7 @@ def get_module(self, module_key: str, program_key: str) -> ModuleNode: .prefetch_related("mentors__github_user") .get(key=module_key, program__key=program_key) ) - except Module.DoesNotExist as err: + except Module.DoesNotExist: msg = f"Module with key '{module_key}' under program '{program_key}' not found." logger.warning(msg, exc_info=True) - raise ObjectDoesNotExist(msg) from err + return None diff --git a/backend/apps/mentorship/api/internal/queries/program.py b/backend/apps/mentorship/api/internal/queries/program.py index 0abf8a263d..fb272e8165 100644 --- a/backend/apps/mentorship/api/internal/queries/program.py +++ b/backend/apps/mentorship/api/internal/queries/program.py @@ -3,7 +3,6 @@ import logging import strawberry -from django.core.exceptions import ObjectDoesNotExist from django.db.models import Q from apps.mentorship.api.internal.nodes.program import PaginatedPrograms, ProgramNode @@ -20,14 +19,14 @@ class ProgramQuery: """Program queries.""" @strawberry.field - def get_program(self, program_key: str) -> ProgramNode: + def get_program(self, program_key: str) -> ProgramNode | None: """Get a program by Key.""" try: program = Program.objects.prefetch_related("admins__github_user").get(key=program_key) - except Program.DoesNotExist as err: + except Program.DoesNotExist: msg = f"Program with key '{program_key}' not found." logger.warning(msg, exc_info=True) - raise ObjectDoesNotExist(msg) from err + return None return program diff --git a/backend/apps/owasp/api/internal/filters/project_health_metrics.py b/backend/apps/owasp/api/internal/filters/project_health_metrics.py index 2e7726cb49..2660c80601 100644 --- a/backend/apps/owasp/api/internal/filters/project_health_metrics.py +++ b/backend/apps/owasp/api/internal/filters/project_health_metrics.py @@ -17,6 +17,6 @@ class ProjectHealthMetricsFilter: @strawberry_django.filter_field # prefix is required for strawberry to work with nested filters # Q is the return type for the filter - def level(self, value: str, prefix: str): + def level(self, value: ProjectLevel, prefix: str): """Filter by project level.""" return Q(project__level=ProjectLevel(value)) if value else Q() diff --git a/backend/apps/owasp/api/internal/nodes/board_of_directors.py b/backend/apps/owasp/api/internal/nodes/board_of_directors.py index 71e9c35577..b12655f533 100644 --- a/backend/apps/owasp/api/internal/nodes/board_of_directors.py +++ b/backend/apps/owasp/api/internal/nodes/board_of_directors.py @@ -10,25 +10,25 @@ @strawberry_django.type( BoardOfDirectors, fields=[ - "year", "created_at", "updated_at", + "year", ], ) class BoardOfDirectorsNode(strawberry.relay.Node): """Board of Directors node.""" - @strawberry.field - def candidates(self) -> list[EntityMemberNode]: + @strawberry_django.field + def candidates(self, root: BoardOfDirectors) -> list[EntityMemberNode]: """Resolve board election candidates.""" - return self.get_candidates() # type: ignore[call-arg] + return root.get_candidates() # type: ignore[call-arg] - @strawberry.field - def members(self) -> list[EntityMemberNode]: + @strawberry_django.field + def members(self, root: BoardOfDirectors) -> list[EntityMemberNode]: """Resolve board members.""" - return self.get_members() # type: ignore[call-arg] + return root.get_members() # type: ignore[call-arg] - @strawberry.field - def owasp_url(self) -> str: + @strawberry_django.field + def owasp_url(self, root: BoardOfDirectors) -> str: """Resolve OWASP board election URL.""" - return self.owasp_url + return root.owasp_url diff --git a/backend/apps/owasp/api/internal/nodes/chapter.py b/backend/apps/owasp/api/internal/nodes/chapter.py index 6e5c29bfb4..82eb4a521d 100644 --- a/backend/apps/owasp/api/internal/nodes/chapter.py +++ b/backend/apps/owasp/api/internal/nodes/chapter.py @@ -33,31 +33,31 @@ class GeoLocationType: class ChapterNode(GenericEntityNode): """Chapter node.""" - @strawberry.field - def contribution_stats(self) -> strawberry.scalars.JSON | None: + @strawberry_django.field + def contribution_stats(self, root: Chapter) -> strawberry.scalars.JSON | None: """Resolve contribution stats with camelCase keys.""" - return deep_camelize(self.contribution_stats) + return deep_camelize(root.contribution_stats) - @strawberry.field - def created_at(self) -> float: + @strawberry_django.field + def created_at(self, root: Chapter) -> float: """Resolve created at.""" - return self.idx_created_at + return root.idx_created_at - @strawberry.field - def geo_location(self) -> GeoLocationType | None: + @strawberry_django.field + def geo_location(self, root: Chapter) -> GeoLocationType | None: """Resolve geographic location.""" return ( - GeoLocationType(lat=self.latitude, lng=self.longitude) - if self.latitude is not None and self.longitude is not None + GeoLocationType(lat=root.latitude, lng=root.longitude) + if root.latitude is not None and root.longitude is not None else None ) - @strawberry.field - def key(self) -> str: + @strawberry_django.field + def key(self, root: Chapter) -> str: """Resolve key.""" - return self.idx_key + return root.idx_key - @strawberry.field - def suggested_location(self) -> str | None: + @strawberry_django.field + def suggested_location(self, root: Chapter) -> str | None: """Resolve suggested location.""" - return self.idx_suggested_location + return root.idx_suggested_location diff --git a/backend/apps/owasp/api/internal/nodes/committee.py b/backend/apps/owasp/api/internal/nodes/committee.py index fd4b83431f..51165cf09e 100644 --- a/backend/apps/owasp/api/internal/nodes/committee.py +++ b/backend/apps/owasp/api/internal/nodes/committee.py @@ -1,6 +1,5 @@ """OWASP committee GraphQL node.""" -import strawberry import strawberry_django from apps.owasp.api.internal.nodes.common import GenericEntityNode @@ -11,32 +10,32 @@ class CommitteeNode(GenericEntityNode): """Committee node.""" - @strawberry.field - def contributors_count(self) -> int: + @strawberry_django.field + def contributors_count(self, root: Committee) -> int: """Resolve contributors count.""" - return self.owasp_repository.contributors_count + return root.owasp_repository.contributors_count if root.owasp_repository else 0 - @strawberry.field - def created_at(self) -> float: + @strawberry_django.field + def created_at(self, root: Committee) -> float | None: """Resolve created at.""" - return self.idx_created_at + return root.idx_created_at - @strawberry.field - def forks_count(self) -> int: + @strawberry_django.field + def forks_count(self, root: Committee) -> int: """Resolve forks count.""" - return self.owasp_repository.forks_count + return root.owasp_repository.forks_count if root.owasp_repository else 0 - @strawberry.field - def issues_count(self) -> int: + @strawberry_django.field + def issues_count(self, root: Committee) -> int: """Resolve issues count.""" - return self.owasp_repository.open_issues_count + return root.owasp_repository.open_issues_count if root.owasp_repository else 0 - @strawberry.field - def repositories_count(self) -> int: + @strawberry_django.field + def repositories_count(self, root: Committee) -> int: """Resolve repositories count.""" return 1 - @strawberry.field - def stars_count(self) -> int: + @strawberry_django.field + def stars_count(self, root: Committee) -> int: """Resolve stars count.""" - return self.owasp_repository.stars_count + return root.owasp_repository.stars_count if root.owasp_repository else 0 diff --git a/backend/apps/owasp/api/internal/nodes/common.py b/backend/apps/owasp/api/internal/nodes/common.py index a692265fad..2a631800f6 100644 --- a/backend/apps/owasp/api/internal/nodes/common.py +++ b/backend/apps/owasp/api/internal/nodes/common.py @@ -1,6 +1,7 @@ """OWASP common GraphQL node.""" import strawberry +import strawberry_django from apps.github.api.internal.nodes.repository_contributor import RepositoryContributorNode from apps.owasp.api.internal.nodes.entity_member import EntityMemberNode @@ -10,32 +11,32 @@ class GenericEntityNode(strawberry.relay.Node): """Base node class for OWASP entities with common fields and resolvers.""" - @strawberry.field - def entity_leaders(self) -> list[EntityMemberNode]: + @strawberry_django.field(prefetch_related=["entity_members__member"]) + def entity_leaders(self, root) -> list[EntityMemberNode]: """Resolve entity leaders.""" - return self.entity_leaders + return root.entity_leaders - @strawberry.field - def leaders(self) -> list[str]: + @strawberry_django.field + def leaders(self, root) -> list[str]: """Resolve leaders.""" - return self.idx_leaders + return root.idx_leaders - @strawberry.field - def related_urls(self) -> list[str]: + @strawberry_django.field + def related_urls(self, root) -> list[str]: """Resolve related URLs.""" - return self.idx_related_urls + return root.related_urls - @strawberry.field - def top_contributors(self) -> list[RepositoryContributorNode]: + @strawberry_django.field + def top_contributors(self, root) -> list[RepositoryContributorNode]: """Resolve top contributors.""" - return [RepositoryContributorNode(**tc) for tc in self.idx_top_contributors] + return [RepositoryContributorNode(**tc) for tc in root.idx_top_contributors] - @strawberry.field - def updated_at(self) -> float: + @strawberry_django.field + def updated_at(self, root) -> float: """Resolve updated at.""" - return self.idx_updated_at + return root.idx_updated_at - @strawberry.field - def url(self) -> str: + @strawberry_django.field + def url(self, root) -> str: """Resolve URL.""" - return self.idx_url + return root.idx_url diff --git a/backend/apps/owasp/api/internal/nodes/member_snapshot.py b/backend/apps/owasp/api/internal/nodes/member_snapshot.py index bb6a6701a2..2319d61ed6 100644 --- a/backend/apps/owasp/api/internal/nodes/member_snapshot.py +++ b/backend/apps/owasp/api/internal/nodes/member_snapshot.py @@ -23,32 +23,32 @@ class MemberSnapshotNode(strawberry.relay.Node): """Member snapshot node.""" - @strawberry.field - def commits_count(self) -> int: + @strawberry_django.field + def commits_count(self, root: MemberSnapshot) -> int: """Resolve commits count.""" - return self.commits_count + return root.commits_count - @strawberry.field - def github_user(self) -> UserNode: + @strawberry_django.field(select_related=["github_user"]) + def github_user(self, root: MemberSnapshot) -> UserNode: """Resolve GitHub user.""" - return self.github_user + return root.github_user - @strawberry.field - def issues_count(self) -> int: + @strawberry_django.field + def issues_count(self, root: MemberSnapshot) -> int: """Resolve issues count.""" - return self.issues_count + return root.issues_count - @strawberry.field - def pull_requests_count(self) -> int: + @strawberry_django.field + def pull_requests_count(self, root: MemberSnapshot) -> int: """Resolve pull requests count.""" - return self.pull_requests_count + return root.pull_requests_count - @strawberry.field - def messages_count(self) -> int: + @strawberry_django.field + def messages_count(self, root: MemberSnapshot) -> int: """Resolve Slack messages count.""" - return self.messages_count + return root.messages_count - @strawberry.field - def total_contributions(self) -> int: + @strawberry_django.field + def total_contributions(self, root: MemberSnapshot) -> int: """Resolve total contributions.""" - return self.total_contributions + return root.total_contributions diff --git a/backend/apps/owasp/api/internal/nodes/project.py b/backend/apps/owasp/api/internal/nodes/project.py index f040a3e5ff..f5da84653f 100644 --- a/backend/apps/owasp/api/internal/nodes/project.py +++ b/backend/apps/owasp/api/internal/nodes/project.py @@ -9,17 +9,19 @@ from apps.github.api.internal.nodes.pull_request import PullRequestNode from apps.github.api.internal.nodes.release import ReleaseNode from apps.github.api.internal.nodes.repository import RepositoryNode +from apps.github.models.milestone import Milestone from apps.owasp.api.internal.nodes.common import GenericEntityNode from apps.owasp.api.internal.nodes.project_health_metrics import ( ProjectHealthMetricsNode, ) from apps.owasp.models.project import Project -from apps.owasp.models.project_health_metrics import ProjectHealthMetrics RECENT_ISSUES_LIMIT = 5 RECENT_RELEASES_LIMIT = 5 RECENT_PULL_REQUESTS_LIMIT = 5 +MAX_LIMIT = 1000 + @strawberry_django.type( Project, @@ -41,84 +43,92 @@ class ProjectNode(GenericEntityNode): """Project node.""" - @strawberry.field - def contribution_stats(self) -> strawberry.scalars.JSON | None: + @strawberry_django.field + def contribution_stats(self, root: Project) -> strawberry.scalars.JSON | None: """Resolve contribution stats with camelCase keys.""" - return deep_camelize(self.contribution_stats) + return deep_camelize(root.contribution_stats) - @strawberry.field - def health_metrics_list(self, limit: int = 30) -> list[ProjectHealthMetricsNode]: + @strawberry_django.field(prefetch_related=["health_metrics"]) + def health_metrics_list( + self, root: Project, limit: int = 30 + ) -> list[ProjectHealthMetricsNode]: """Resolve project health metrics.""" - return ProjectHealthMetrics.objects.filter( - project=self, - ).order_by( - "nest_created_at", - )[:limit] - - @strawberry.field - def health_metrics_latest(self) -> ProjectHealthMetricsNode | None: - """Resolve latest project health metrics.""" return ( - ProjectHealthMetrics.get_latest_health_metrics() - .filter( - project=self, - ) - .first() + root.health_metrics.order_by("nest_created_at")[:limit] + if (limit := min(limit, MAX_LIMIT)) > 0 + else [] ) - @strawberry.field - def issues_count(self) -> int: + @strawberry_django.field(prefetch_related=["health_metrics"]) + def health_metrics_latest(self, root: Project) -> ProjectHealthMetricsNode | None: + """Resolve latest project health metrics.""" + return root.health_metrics.order_by("-nest_created_at").first() + + @strawberry_django.field + def issues_count(self, root: Project) -> int: """Resolve issues count.""" - return self.idx_issues_count + return root.idx_issues_count - @strawberry.field - def key(self) -> str: + @strawberry_django.field + def key(self, root: Project) -> str: """Resolve key.""" - return self.idx_key + return root.idx_key - @strawberry.field - def languages(self) -> list[str]: + @strawberry_django.field + def languages(self, root: Project) -> list[str]: """Resolve languages.""" - return self.idx_languages + return root.idx_languages - @strawberry.field - def recent_issues(self) -> list[IssueNode]: + @strawberry_django.field + def recent_issues(self, root: Project) -> list[IssueNode]: """Resolve recent issues.""" - return self.issues.select_related("author").order_by("-created_at")[:RECENT_ISSUES_LIMIT] + return root.issues.order_by("-created_at")[:RECENT_ISSUES_LIMIT] - @strawberry.field - def recent_milestones(self, limit: int = 5) -> list[MilestoneNode]: + @strawberry_django.field + def recent_milestones(self, root: Project, limit: int = 5) -> list[MilestoneNode]: """Resolve recent milestones.""" - return self.recent_milestones.select_related("author").order_by("-created_at")[:limit] + return ( + Milestone.objects.filter( + repository__in=root.repositories.all(), + ) + .select_related( + "repository__organization", + "author__owasp_profile", + ) + .prefetch_related( + "labels", + ) + .order_by("-created_at")[:limit] + if (limit := min(limit, MAX_LIMIT)) > 0 + else [] + ) - @strawberry.field - def recent_pull_requests(self) -> list[PullRequestNode]: + @strawberry_django.field + def recent_pull_requests(self, root: Project) -> list[PullRequestNode]: """Resolve recent pull requests.""" - return self.pull_requests.select_related("author").order_by("-created_at")[ - :RECENT_PULL_REQUESTS_LIMIT - ] + return root.pull_requests.order_by("-created_at")[:RECENT_PULL_REQUESTS_LIMIT] - @strawberry.field - def recent_releases(self) -> list[ReleaseNode]: + @strawberry_django.field + def recent_releases(self, root: Project) -> list[ReleaseNode]: """Resolve recent releases.""" - return self.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] + return root.published_releases.order_by("-published_at")[:RECENT_RELEASES_LIMIT] - @strawberry.field - def repositories(self) -> list[RepositoryNode]: + @strawberry_django.field(prefetch_related=["repositories"]) + def repositories(self, root: Project) -> list[RepositoryNode]: """Resolve repositories.""" - return self.repositories.filter( + return root.repositories.filter( organization__isnull=False, ).order_by( "-pushed_at", "-updated_at", ) - @strawberry.field - def repositories_count(self) -> int: + @strawberry_django.field + def repositories_count(self, root: Project) -> int: """Resolve repositories count.""" - return self.idx_repositories_count + return root.idx_repositories_count - @strawberry.field - def topics(self) -> list[str]: + @strawberry_django.field + def topics(self, root: Project) -> list[str]: """Resolve topics.""" - return self.idx_topics + return root.idx_topics diff --git a/backend/apps/owasp/api/internal/nodes/project_health_metrics.py b/backend/apps/owasp/api/internal/nodes/project_health_metrics.py index ce29c9dfcc..067b79ae79 100644 --- a/backend/apps/owasp/api/internal/nodes/project_health_metrics.py +++ b/backend/apps/owasp/api/internal/nodes/project_health_metrics.py @@ -31,67 +31,67 @@ class ProjectHealthMetricsNode(strawberry.relay.Node): """Project health metrics node.""" - @strawberry.field - def age_days(self) -> int: + @strawberry_django.field + def age_days(self, root: ProjectHealthMetrics) -> int: """Resolve project age in days.""" - return self.age_days + return root.age_days - @strawberry.field - def age_days_requirement(self) -> int: + @strawberry_django.field + def age_days_requirement(self, root: ProjectHealthMetrics) -> int: """Resolve project age requirement in days.""" - return self.age_days_requirement + return root.age_days_requirement - @strawberry.field - def created_at(self) -> datetime: + @strawberry_django.field + def created_at(self, root: ProjectHealthMetrics) -> datetime: """Resolve metrics creation date.""" - return self.nest_created_at + return root.nest_created_at - @strawberry.field - def last_commit_days(self) -> int: + @strawberry_django.field + def last_commit_days(self, root: ProjectHealthMetrics) -> int: """Resolve last commit age in days.""" - return self.last_commit_days + return root.last_commit_days - @strawberry.field - def last_commit_days_requirement(self) -> int: + @strawberry_django.field + def last_commit_days_requirement(self, root: ProjectHealthMetrics) -> int: """Resolve last commit age requirement in days.""" - return self.last_commit_days_requirement + return root.last_commit_days_requirement - @strawberry.field - def last_pull_request_days(self) -> int: + @strawberry_django.field + def last_pull_request_days(self, root: ProjectHealthMetrics) -> int: """Resolve last pull request age in days.""" - return self.last_pull_request_days + return root.last_pull_request_days - @strawberry.field - def last_pull_request_days_requirement(self) -> int: + @strawberry_django.field + def last_pull_request_days_requirement(self, root: ProjectHealthMetrics) -> int: """Resolve last pull request age requirement in days.""" - return self.last_pull_request_days_requirement + return root.last_pull_request_days_requirement - @strawberry.field - def last_release_days(self) -> int: + @strawberry_django.field + def last_release_days(self, root: ProjectHealthMetrics) -> int: """Resolve last release age in days.""" - return self.last_release_days + return root.last_release_days - @strawberry.field - def last_release_days_requirement(self) -> int: + @strawberry_django.field + def last_release_days_requirement(self, root: ProjectHealthMetrics) -> int: """Resolve last release age requirement in days.""" - return self.last_release_days_requirement + return root.last_release_days_requirement - @strawberry.field - def project_key(self) -> str: + @strawberry_django.field(select_related=["project"]) + def project_key(self, root: ProjectHealthMetrics) -> str: """Resolve project key.""" - return self.project.nest_key + return root.project.nest_key - @strawberry.field - def project_name(self) -> str: + @strawberry_django.field(select_related=["project"]) + def project_name(self, root: ProjectHealthMetrics) -> str: """Resolve project name.""" - return self.project.name + return root.project.name - @strawberry.field - def owasp_page_last_update_days(self) -> int: + @strawberry_django.field + def owasp_page_last_update_days(self, root: ProjectHealthMetrics) -> int: """Resolve OWASP page last update age in days.""" - return self.owasp_page_last_update_days + return root.owasp_page_last_update_days - @strawberry.field - def owasp_page_last_update_days_requirement(self) -> int: + @strawberry_django.field + def owasp_page_last_update_days_requirement(self, root: ProjectHealthMetrics) -> int: """Resolve OWASP page last update age requirement in days.""" - return self.owasp_page_last_update_days_requirement + return root.owasp_page_last_update_days_requirement diff --git a/backend/apps/owasp/api/internal/nodes/project_health_stats.py b/backend/apps/owasp/api/internal/nodes/project_health_stats.py index 295d6be276..c005128ebd 100644 --- a/backend/apps/owasp/api/internal/nodes/project_health_stats.py +++ b/backend/apps/owasp/api/internal/nodes/project_health_stats.py @@ -7,7 +7,7 @@ class ProjectHealthStatsNode: """Node representing overall health stats of OWASP projects.""" - average_score: float + average_score: float | None monthly_overall_scores: list[float] monthly_overall_scores_months: list[int] projects_count_healthy: int diff --git a/backend/apps/owasp/api/internal/nodes/snapshot.py b/backend/apps/owasp/api/internal/nodes/snapshot.py index 0e67f09509..4e45ab89eb 100644 --- a/backend/apps/owasp/api/internal/nodes/snapshot.py +++ b/backend/apps/owasp/api/internal/nodes/snapshot.py @@ -7,7 +7,6 @@ from apps.github.api.internal.nodes.release import ReleaseNode from apps.github.api.internal.nodes.user import UserNode from apps.owasp.api.internal.nodes.chapter import ChapterNode -from apps.owasp.api.internal.nodes.common import GenericEntityNode from apps.owasp.api.internal.nodes.project import ProjectNode from apps.owasp.models.snapshot import Snapshot @@ -23,35 +22,32 @@ "title", ], ) -class SnapshotNode(GenericEntityNode): +class SnapshotNode(strawberry.relay.Node): """Snapshot node.""" - @strawberry.field - def key(self) -> str: - """Resolve key.""" - return self.key + new_chapters: list[ChapterNode] = strawberry_django.field() - @strawberry.field - def new_chapters(self) -> list[ChapterNode]: - """Resolve new chapters.""" - return self.new_chapters.all() + @strawberry_django.field + def key(self, root: Snapshot) -> str: + """Resolve key.""" + return root.key - @strawberry.field - def new_issues(self) -> list[IssueNode]: + @strawberry_django.field(prefetch_related=["new_issues"]) + def new_issues(self, root: Snapshot) -> list[IssueNode]: """Resolve new issues.""" - return self.new_issues.order_by("-created_at")[:RECENT_ISSUES_LIMIT] + return root.new_issues.order_by("-created_at")[:RECENT_ISSUES_LIMIT] - @strawberry.field - def new_projects(self) -> list[ProjectNode]: + @strawberry_django.field(prefetch_related=["new_projects"]) + def new_projects(self, root: Snapshot) -> list[ProjectNode]: """Resolve new projects.""" - return self.new_projects.order_by("-created_at") + return root.new_projects.order_by("-created_at") - @strawberry.field - def new_releases(self) -> list[ReleaseNode]: + @strawberry_django.field(prefetch_related=["new_releases"]) + def new_releases(self, root: Snapshot) -> list[ReleaseNode]: """Resolve new releases.""" - return self.new_releases.order_by("-published_at") + return root.new_releases.order_by("-published_at") - @strawberry.field - def new_users(self) -> list[UserNode]: + @strawberry_django.field(prefetch_related=["new_users"]) + def new_users(self, root: Snapshot) -> list[UserNode]: """Resolve new users.""" - return self.new_users.order_by("-created_at") + return root.new_users.order_by("-created_at") diff --git a/backend/apps/owasp/api/internal/permissions/project_health_metrics.py b/backend/apps/owasp/api/internal/permissions/project_health_metrics.py index aa75f689ec..9ad156ecac 100644 --- a/backend/apps/owasp/api/internal/permissions/project_health_metrics.py +++ b/backend/apps/owasp/api/internal/permissions/project_health_metrics.py @@ -1,5 +1,6 @@ """Strawberry Permission Classes for Project Health Metrics.""" +from django.conf import settings from strawberry.permission import BasePermission @@ -11,7 +12,11 @@ class HasDashboardAccess(BasePermission): def has_permission(self, source, info, **kwargs) -> bool: """Check if the user has dashboard access.""" return ( - (user := info.context.request.user) - and user.is_authenticated - and user.github_user.is_owasp_staff + True + if settings.IS_E2E_ENVIRONMENT or settings.IS_FUZZ_ENVIRONMENT + else ( + (user := info.context.request.user) + and user.is_authenticated + and user.github_user.is_owasp_staff + ) ) diff --git a/backend/apps/owasp/api/internal/queries/board_of_directors.py b/backend/apps/owasp/api/internal/queries/board_of_directors.py index 9445386a03..df611022c3 100644 --- a/backend/apps/owasp/api/internal/queries/board_of_directors.py +++ b/backend/apps/owasp/api/internal/queries/board_of_directors.py @@ -1,16 +1,19 @@ """OWASP Board of Directors GraphQL queries.""" import strawberry +import strawberry_django from apps.owasp.api.internal.nodes.board_of_directors import BoardOfDirectorsNode from apps.owasp.models.board_of_directors import BoardOfDirectors +MAX_LIMIT = 1000 + @strawberry.type class BoardOfDirectorsQuery: """GraphQL queries for Board of Directors model.""" - @strawberry.field + @strawberry_django.field def board_of_directors(self, year: int) -> BoardOfDirectorsNode | None: """Resolve Board of Directors by year. @@ -26,7 +29,7 @@ def board_of_directors(self, year: int) -> BoardOfDirectorsNode | None: except BoardOfDirectors.DoesNotExist: return None - @strawberry.field + @strawberry_django.field def boards_of_directors(self, limit: int = 10) -> list[BoardOfDirectorsNode]: """Resolve multiple Board of Directors instances. @@ -37,4 +40,8 @@ def boards_of_directors(self, limit: int = 10) -> list[BoardOfDirectorsNode]: List of BoardOfDirectorsNode objects. """ - return BoardOfDirectors.objects.order_by("-year")[:limit] + return ( + BoardOfDirectors.objects.order_by("-year")[:limit] + if (limit := min(limit, MAX_LIMIT)) > 0 + else [] + ) diff --git a/backend/apps/owasp/api/internal/queries/chapter.py b/backend/apps/owasp/api/internal/queries/chapter.py index 8abebb5b5f..64ecfd7f41 100644 --- a/backend/apps/owasp/api/internal/queries/chapter.py +++ b/backend/apps/owasp/api/internal/queries/chapter.py @@ -1,16 +1,19 @@ """OWASP chapter GraphQL queries.""" import strawberry +import strawberry_django from apps.owasp.api.internal.nodes.chapter import ChapterNode from apps.owasp.models.chapter import Chapter +MAX_LIMIT = 1000 + @strawberry.type class ChapterQuery: """Chapter queries.""" - @strawberry.field + @strawberry_django.field def chapter(self, key: str) -> ChapterNode | None: """Resolve chapter.""" try: @@ -18,7 +21,11 @@ def chapter(self, key: str) -> ChapterNode | None: except Chapter.DoesNotExist: return None - @strawberry.field + @strawberry_django.field def recent_chapters(self, limit: int = 8) -> list[ChapterNode]: """Resolve recent chapters.""" - return Chapter.active_chapters.order_by("-created_at")[:limit] + return ( + Chapter.active_chapters.order_by("-created_at")[:limit] + if (limit := min(limit, MAX_LIMIT)) > 0 + else [] + ) diff --git a/backend/apps/owasp/api/internal/queries/committee.py b/backend/apps/owasp/api/internal/queries/committee.py index 4cbdb7f3bd..fb7bf37ba1 100644 --- a/backend/apps/owasp/api/internal/queries/committee.py +++ b/backend/apps/owasp/api/internal/queries/committee.py @@ -1,6 +1,7 @@ """OWASP committee GraphQL queries.""" import strawberry +import strawberry_django from apps.owasp.api.internal.nodes.committee import CommitteeNode from apps.owasp.models.committee import Committee @@ -10,7 +11,7 @@ class CommitteeQuery: """Committee queries.""" - @strawberry.field + @strawberry_django.field def committee(self, key: str) -> CommitteeNode | None: """Resolve committee by key. diff --git a/backend/apps/owasp/api/internal/queries/event.py b/backend/apps/owasp/api/internal/queries/event.py index b08973201a..68dad6941a 100644 --- a/backend/apps/owasp/api/internal/queries/event.py +++ b/backend/apps/owasp/api/internal/queries/event.py @@ -1,16 +1,19 @@ """OWASP event GraphQL queries.""" import strawberry +import strawberry_django from apps.owasp.api.internal.nodes.event import EventNode from apps.owasp.models.event import Event +MAX_LIMIT = 1000 + @strawberry.type class EventQuery: """Event queries.""" - @strawberry.field + @strawberry_django.field def upcoming_events(self, limit: int = 6) -> list[EventNode]: """Resolve upcoming events.""" - return Event.upcoming_events()[:limit] + return Event.upcoming_events()[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else [] diff --git a/backend/apps/owasp/api/internal/queries/member_snapshot.py b/backend/apps/owasp/api/internal/queries/member_snapshot.py index f48b03f01b..4aac59e04e 100644 --- a/backend/apps/owasp/api/internal/queries/member_snapshot.py +++ b/backend/apps/owasp/api/internal/queries/member_snapshot.py @@ -1,17 +1,20 @@ """OWASP member snapshot GraphQL queries.""" import strawberry +import strawberry_django from apps.github.models.user import User from apps.owasp.api.internal.nodes.member_snapshot import MemberSnapshotNode from apps.owasp.models.member_snapshot import MemberSnapshot +MAX_LIMIT = 1000 + @strawberry.type class MemberSnapshotQuery: """GraphQL queries for MemberSnapshot model.""" - @strawberry.field + @strawberry_django.field def member_snapshot( self, user_login: str, start_year: int | None = None ) -> MemberSnapshotNode | None: @@ -28,7 +31,11 @@ def member_snapshot( try: user = User.objects.get(login=user_login) - query = MemberSnapshot.objects.filter(github_user=user) + query = ( + MemberSnapshot.objects.select_related("github_user") + .prefetch_related("issues", "pull_requests", "messages") + .filter(github_user=user) + ) if start_year: query = query.filter(start_at__year=start_year) @@ -38,7 +45,7 @@ def member_snapshot( except User.DoesNotExist: return None - @strawberry.field + @strawberry_django.field def member_snapshots( self, user_login: str | None = None, limit: int = 10 ) -> list[MemberSnapshotNode]: @@ -52,13 +59,14 @@ def member_snapshots( List of MemberSnapshotNode objects """ - query = MemberSnapshot.objects.all() + snapshots = MemberSnapshot.objects.all().select_related("github_user") if user_login: try: - user = User.objects.get(login=user_login) - query = query.filter(github_user=user) + snapshots = snapshots.filter(github_user=User.objects.get(login=user_login)) except User.DoesNotExist: return [] - return query.order_by("-start_at")[:limit] + return ( + snapshots.order_by("-start_at")[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else [] + ) diff --git a/backend/apps/owasp/api/internal/queries/post.py b/backend/apps/owasp/api/internal/queries/post.py index 28c1d88817..a7f4ca3d23 100644 --- a/backend/apps/owasp/api/internal/queries/post.py +++ b/backend/apps/owasp/api/internal/queries/post.py @@ -1,16 +1,19 @@ """OWASP event GraphQL queries.""" import strawberry +import strawberry_django from apps.owasp.api.internal.nodes.post import PostNode from apps.owasp.models.post import Post +MAX_LIMIT = 1000 + @strawberry.type class PostQuery: """GraphQL queries for Post model.""" - @strawberry.field + @strawberry_django.field def recent_posts(self, limit: int = 5) -> list[PostNode]: - """Return the 5 most recent posts.""" - return Post.recent_posts()[:limit] + """Return the most recent posts.""" + return Post.recent_posts()[:limit] if (limit := min(limit, MAX_LIMIT)) > 0 else [] diff --git a/backend/apps/owasp/api/internal/queries/project.py b/backend/apps/owasp/api/internal/queries/project.py index 154e25bfe9..af4db3c009 100644 --- a/backend/apps/owasp/api/internal/queries/project.py +++ b/backend/apps/owasp/api/internal/queries/project.py @@ -1,18 +1,24 @@ """OWASP project GraphQL queries.""" import strawberry +import strawberry_django from django.db.models import Q from apps.github.models.user import User as GithubUser from apps.owasp.api.internal.nodes.project import ProjectNode from apps.owasp.models.project import Project +MAX_RECENT_PROJECTS_LIMIT = 1000 +MAX_SEARCH_QUERY_LENGTH = 100 +MIN_SEARCH_QUERY_LENGTH = 3 +SEARCH_PROJECTS_LIMIT = 3 + @strawberry.type class ProjectQuery: """Project queries.""" - @strawberry.field + @strawberry_django.field def project(self, key: str) -> ProjectNode | None: """Resolve project. @@ -28,7 +34,7 @@ def project(self, key: str) -> ProjectNode | None: except Project.DoesNotExist: return None - @strawberry.field + @strawberry_django.field def recent_projects(self, limit: int = 8) -> list[ProjectNode]: """Resolve recent projects. @@ -39,20 +45,28 @@ def recent_projects(self, limit: int = 8) -> list[ProjectNode]: list[ProjectNode]: A list of recent active projects. """ - return Project.objects.filter(is_active=True).order_by("-created_at")[:limit] + return ( + Project.objects.filter(is_active=True).order_by("-created_at")[:limit] + if (limit := min(limit, MAX_RECENT_PROJECTS_LIMIT)) > 0 + else [] + ) - @strawberry.field + @strawberry_django.field def search_projects(self, query: str) -> list[ProjectNode]: """Search active projects by name (case-insensitive, partial match).""" - if not query.strip(): + cleaned_query = query.strip() + if ( + len(cleaned_query) < MIN_SEARCH_QUERY_LENGTH + or len(cleaned_query) > MAX_SEARCH_QUERY_LENGTH + ): return [] return Project.objects.filter( is_active=True, - name__icontains=query.strip(), - ).order_by("name")[:3] + name__icontains=cleaned_query, + ).order_by("name")[:SEARCH_PROJECTS_LIMIT] - @strawberry.field + @strawberry_django.field def is_project_leader(self, info: strawberry.Info, login: str) -> bool: """Check if a GitHub login or name is listed as a project leader.""" try: diff --git a/backend/apps/owasp/api/internal/queries/project_health_metrics.py b/backend/apps/owasp/api/internal/queries/project_health_metrics.py index ead03bf330..d133da590a 100644 --- a/backend/apps/owasp/api/internal/queries/project_health_metrics.py +++ b/backend/apps/owasp/api/internal/queries/project_health_metrics.py @@ -2,6 +2,7 @@ import strawberry import strawberry_django +from strawberry.types.unset import UNSET from apps.owasp.api.internal.filters.project_health_metrics import ProjectHealthMetricsFilter from apps.owasp.api.internal.nodes.project_health_metrics import ProjectHealthMetricsNode @@ -10,6 +11,9 @@ from apps.owasp.api.internal.permissions.project_health_metrics import HasDashboardAccess from apps.owasp.models.project_health_metrics import ProjectHealthMetrics +MAX_LIMIT = 1000 +MAX_OFFSET = 10000 + @strawberry.type class ProjectHealthMetricsQuery: @@ -39,9 +43,19 @@ def project_health_metrics( list[ProjectHealthMetricsNode]: List of project health metrics. """ + if pagination: + if pagination.offset < 0: + return [] + pagination.offset = min(pagination.offset, MAX_OFFSET) + + if pagination.limit is not None and pagination.limit != UNSET: + if pagination.limit <= 0: + return [] + pagination.limit = min(pagination.limit, MAX_LIMIT) + return ProjectHealthMetrics.get_latest_health_metrics() - @strawberry.field( + @strawberry_django.field( permission_classes=[HasDashboardAccess], ) def project_health_stats(self) -> ProjectHealthStatsNode: diff --git a/backend/apps/owasp/api/internal/queries/snapshot.py b/backend/apps/owasp/api/internal/queries/snapshot.py index a70c8d264d..d649acad34 100644 --- a/backend/apps/owasp/api/internal/queries/snapshot.py +++ b/backend/apps/owasp/api/internal/queries/snapshot.py @@ -1,16 +1,19 @@ """OWASP snapshot GraphQL queries.""" import strawberry +import strawberry_django from apps.owasp.api.internal.nodes.snapshot import SnapshotNode from apps.owasp.models.snapshot import Snapshot +MAX_LIMIT = 100 + @strawberry.type class SnapshotQuery: """Snapshot queries.""" - @strawberry.field + @strawberry_django.field def snapshot(self, key: str) -> SnapshotNode | None: """Resolve snapshot by key.""" try: @@ -21,11 +24,15 @@ def snapshot(self, key: str) -> SnapshotNode | None: except Snapshot.DoesNotExist: return None - @strawberry.field + @strawberry_django.field def snapshots(self, limit: int = 12) -> list[SnapshotNode]: """Resolve snapshots.""" - return Snapshot.objects.filter( - status=Snapshot.Status.COMPLETED, - ).order_by( - "-created_at", - )[:limit] + return ( + Snapshot.objects.filter( + status=Snapshot.Status.COMPLETED, + ).order_by( + "-created_at", + )[:limit] + if (limit := min(limit, MAX_LIMIT)) > 0 + else [] + ) diff --git a/backend/apps/owasp/api/internal/queries/sponsor.py b/backend/apps/owasp/api/internal/queries/sponsor.py index 3e692a7ec8..80014e529f 100644 --- a/backend/apps/owasp/api/internal/queries/sponsor.py +++ b/backend/apps/owasp/api/internal/queries/sponsor.py @@ -1,6 +1,7 @@ """OWASP sponsors GraphQL queries.""" import strawberry +import strawberry_django from apps.owasp.api.internal.nodes.sponsor import SponsorNode from apps.owasp.models.sponsor import Sponsor @@ -10,7 +11,7 @@ class SponsorQuery: """Sponsor queries.""" - @strawberry.field + @strawberry_django.field def sponsors(self) -> list[SponsorNode]: """Resolve sponsors.""" return sorted( diff --git a/backend/apps/owasp/api/internal/views/permissions.py b/backend/apps/owasp/api/internal/views/permissions.py index c193e0c985..63d94ee302 100644 --- a/backend/apps/owasp/api/internal/views/permissions.py +++ b/backend/apps/owasp/api/internal/views/permissions.py @@ -2,12 +2,17 @@ from functools import wraps +from django.conf import settings from django.http import HttpResponseForbidden def has_dashboard_permission(request): """Check if user has dashboard access.""" - return (user := request.user) and user.is_authenticated and user.github_user.is_owasp_staff + return ( + True + if settings.IS_E2E_ENVIRONMENT or settings.IS_FUZZ_ENVIRONMENT + else (user := request.user) and user.is_authenticated and user.github_user.is_owasp_staff + ) def dashboard_access_required(view_func): diff --git a/backend/apps/owasp/migrations/0070_snapshot_owasp_snapshot_created_at_idx_and_more.py b/backend/apps/owasp/migrations/0070_snapshot_owasp_snapshot_created_at_idx_and_more.py new file mode 100644 index 0000000000..b9c3133229 --- /dev/null +++ b/backend/apps/owasp/migrations/0070_snapshot_owasp_snapshot_created_at_idx_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 6.0 on 2026-01-06 09:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0040_merge_20251117_0136"), + ("owasp", "0069_alter_project_contribution_data_and_more"), + ] + + operations = [ + migrations.AddIndex( + model_name="snapshot", + index=models.Index(fields=["-created_at"], name="owasp_snapshot_created_at_idx"), + ), + migrations.AddIndex( + model_name="snapshot", + index=models.Index(fields=["key", "status"], name="owasp_snapshot_key_status_idx"), + ), + ] diff --git a/backend/apps/owasp/migrations/0071_trigram_extension.py b/backend/apps/owasp/migrations/0071_trigram_extension.py new file mode 100644 index 0000000000..123b3876dd --- /dev/null +++ b/backend/apps/owasp/migrations/0071_trigram_extension.py @@ -0,0 +1,14 @@ +# Generated by Django 6.0 on 2026-01-07 11:29 + +from django.contrib.postgres.operations import TrigramExtension +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0070_snapshot_owasp_snapshot_created_at_idx_and_more"), + ] + + operations = [ + TrigramExtension(), + ] diff --git a/backend/apps/owasp/migrations/0072_project_project_name_gin_idx_and_more.py b/backend/apps/owasp/migrations/0072_project_project_name_gin_idx_and_more.py new file mode 100644 index 0000000000..f7367a2404 --- /dev/null +++ b/backend/apps/owasp/migrations/0072_project_project_name_gin_idx_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 6.0 on 2026-01-07 12:00 + +import django.contrib.postgres.indexes +import django.db.models.functions.comparison +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("github", "0040_merge_20251117_0136"), + ("owasp", "0071_trigram_extension"), + ] + + operations = [ + migrations.AddIndex( + model_name="project", + index=django.contrib.postgres.indexes.GinIndex( + condition=models.Q(("is_active", True)), + fields=["name"], + name="project_name_gin_idx", + opclasses=["gin_trgm_ops"], + ), + ), + migrations.AddIndex( + model_name="project", + index=django.contrib.postgres.indexes.GinIndex( + django.contrib.postgres.indexes.OpClass( + django.db.models.functions.comparison.Cast("leaders_raw", models.TextField()), + name="gin_trgm_ops", + ), + name="project_leaders_raw_gin_idx", + ), + ), + ] diff --git a/backend/apps/owasp/models/chapter.py b/backend/apps/owasp/models/chapter.py index fd199e12af..963e1be5db 100644 --- a/backend/apps/owasp/models/chapter.py +++ b/backend/apps/owasp/models/chapter.py @@ -8,7 +8,6 @@ from django.db import models from apps.common.geocoding import get_location_coordinates -from apps.common.index import IndexBase from apps.common.models import BulkSaveModel, TimestampedModel from apps.common.open_ai import OpenAi from apps.common.utils import get_absolute_url, join_values @@ -98,7 +97,12 @@ def nest_url(self) -> str: @lru_cache def active_chapters_count(): """Return active chapters count.""" - return IndexBase.get_total_count("chapters", search_filters="idx_is_active:true") + return Chapter.objects.filter( + is_active=True, + latitude__isnull=False, + longitude__isnull=False, + owasp_repository__is_empty=False, + ).count() def from_github(self, repository) -> None: """Update instance based on GitHub repository data. diff --git a/backend/apps/owasp/models/committee.py b/backend/apps/owasp/models/committee.py index 658acc718f..aed85a5188 100644 --- a/backend/apps/owasp/models/committee.py +++ b/backend/apps/owasp/models/committee.py @@ -5,7 +5,6 @@ from django.contrib.contenttypes.fields import GenericRelation from django.db import models -from apps.common.index import IndexBase from apps.common.models import BulkSaveModel, TimestampedModel from apps.core.models.prompt import Prompt from apps.owasp.models.common import RepositoryBasedEntityModel @@ -67,7 +66,9 @@ def nest_key(self): @lru_cache def active_committees_count(): """Return active committees count.""" - return IndexBase.get_total_count("committees") + return Committee.objects.filter( + has_active_repositories=True, + ).count() @staticmethod def bulk_save(committees, fields=None) -> None: # type: ignore[override] diff --git a/backend/apps/owasp/models/common.py b/backend/apps/owasp/models/common.py index 79193122f7..376613a810 100644 --- a/backend/apps/owasp/models/common.py +++ b/backend/apps/owasp/models/common.py @@ -2,12 +2,13 @@ from __future__ import annotations -import itertools import logging import re +from functools import cached_property from urllib.parse import urlparse import yaml +from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType from django.db import models @@ -89,16 +90,18 @@ class Meta: blank=True, ) - @property - def entity_leaders(self) -> models.QuerySet[EntityMember]: + # GRs. + entity_members = GenericRelation( + EntityMember, + content_type_field="entity_type", + object_id_field="entity_id", + related_query_name="entity", + ) + + @cached_property + def entity_leaders(self) -> list[EntityMember]: """Return entity's leaders.""" - return EntityMember.objects.filter( - entity_id=self.id, - entity_type=ContentType.objects.get_for_model(self.__class__), - is_active=True, - is_reviewed=True, - role=EntityMember.Role.LEADER, - ).order_by("order") + return self.entity_members.filter(role=EntityMember.Role.LEADER).order_by("order") @property def github_url(self) -> str: @@ -197,18 +200,26 @@ def get_leaders(self): return [] leaders = [] + # Compile regex patterns once per method call (before loop). + re_bracketed_pattern = re.compile( + r"[-*]\s{0,3}\[\s{0,3}([^\]\(]{1,200})(?:\s{0,3}\([^)]{0,100}\))?\s{0,3}\]" + ) + re_plain_pattern = re.compile(r"\*\s{0,3}([\w\s]{1,200})") + re_parenthetical_cleanup_pattern = re.compile(r"\s{0,3}\([^)]{0,100}\)\s{0,3}$") for line in content.split("\n"): - leaders.extend( - [ - name - for name in itertools.chain( - *re.findall( - r"[-*]\s*\[\s*([^(]+?)\s*(?:\([^)]*\))?\]|\*\s*([\w\s]+)", line.strip() - ) - ) - if name.strip() - ] - ) + stripped_line = line.strip() + names = [] + + names.extend(re_bracketed_pattern.findall(stripped_line)) + names.extend(re_plain_pattern.findall(stripped_line)) + + cleaned_names = [] + for raw_name in names: + if raw_name.strip(): + cleaned = re_parenthetical_cleanup_pattern.sub("", raw_name).strip() + cleaned_names.append(cleaned) + + leaders.extend(cleaned_names) return leaders diff --git a/backend/apps/owasp/models/mixins/committee.py b/backend/apps/owasp/models/mixins/committee.py index d11d0312af..7c3ea7615c 100644 --- a/backend/apps/owasp/models/mixins/committee.py +++ b/backend/apps/owasp/models/mixins/committee.py @@ -12,7 +12,7 @@ class CommitteeIndexMixin(RepositoryBasedEntityModelMixin): @property def idx_created_at(self): """Return created at for indexing.""" - return self.created_at.timestamp() + return self.created_at.timestamp() if self.created_at else None @property def idx_key(self): @@ -32,4 +32,4 @@ def idx_top_contributors(self) -> list[str]: @property def idx_updated_at(self): """Return updated at for indexing.""" - return self.updated_at.timestamp() + return self.updated_at.timestamp() if self.updated_at else None diff --git a/backend/apps/owasp/models/project.py b/backend/apps/owasp/models/project.py index 9c3af5c1ba..0a6ecdcafb 100644 --- a/backend/apps/owasp/models/project.py +++ b/backend/apps/owasp/models/project.py @@ -7,10 +7,10 @@ from typing import TYPE_CHECKING from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.postgres.indexes import GinIndex, OpClass from django.db import models from django.utils import timezone -from apps.common.index import IndexBase from apps.common.models import BulkSaveModel, TimestampedModel from apps.common.utils import get_absolute_url from apps.core.models.prompt import Prompt @@ -26,10 +26,10 @@ ) from apps.owasp.models.managers.project import ActiveProjectManager from apps.owasp.models.mixins.project import ProjectIndexMixin -from apps.owasp.models.project_health_metrics import ProjectHealthMetrics if TYPE_CHECKING: from apps.owasp.models.entity_member import EntityMember + from apps.owasp.models.project_health_metrics import ProjectHealthMetrics MAX_LEADERS_COUNT = 5 @@ -50,6 +50,18 @@ class Meta: indexes = [ models.Index(fields=["-created_at"], name="project_created_at_desc_idx"), models.Index(fields=["-updated_at"], name="project_updated_at_desc_idx"), + GinIndex( + fields=["name"], + name="project_name_gin_idx", + opclasses=["gin_trgm_ops"], + condition=models.Q(is_active=True), + ), + GinIndex( + OpClass( + models.functions.Cast("leaders_raw", models.TextField()), name="gin_trgm_ops" + ), + name="project_leaders_raw_gin_idx", + ), ] verbose_name_plural = "Projects" @@ -146,7 +158,7 @@ def __str__(self) -> str: return f"{self.name or self.key}" @property - def entity_leaders(self) -> models.QuerySet[EntityMember]: + def entity_leaders(self) -> list[EntityMember]: """Return project leaders.""" return super().entity_leaders[:MAX_LEADERS_COUNT] @@ -185,10 +197,20 @@ def is_tool_type(self) -> bool: @property def issues(self): """Return issues.""" - return Issue.objects.filter( - repository__in=self.repositories.all(), - ).select_related( - "repository", + return ( + Issue.objects.filter( + repository__in=self.repositories.all(), + ) + .select_related( + "author", + "level", + "milestone", + "repository", + ) + .prefetch_related( + "assignees", + "labels", + ) ) @property @@ -199,9 +221,7 @@ def issues_count(self) -> int: @property def last_health_metrics(self) -> ProjectHealthMetrics | None: """Return last health metrics for the project.""" - return ( - ProjectHealthMetrics.objects.filter(project=self).order_by("-nest_created_at").first() - ) + return self.health_metrics.order_by("-nest_created_at").first() @property def leaders_count(self) -> int: @@ -240,10 +260,20 @@ def owasp_page_last_updated_at(self) -> datetime.datetime | None: @property def pull_requests(self): """Return pull requests.""" - return PullRequest.objects.filter( - repository__in=self.repositories.all(), - ).select_related( - "repository", + return ( + PullRequest.objects.filter( + repository__in=self.repositories.all(), + ) + .select_related( + "author", + "milestone", + "repository__organization", + "repository", + ) + .prefetch_related( + "assignees", + "labels", + ) ) @property @@ -266,16 +296,25 @@ def published_releases(self): published_at__isnull=False, repository__in=self.repositories.all(), ).select_related( + "author", "repository", + "repository__organization", ) @property def recent_milestones(self): """Return recent milestones.""" - return Milestone.objects.filter( - repository__in=self.repositories.all(), - ).select_related( - "repository", + return ( + Milestone.objects.filter( + repository__in=self.repositories.all(), + ) + .select_related( + "author", + "repository", + ) + .prefetch_related( + "labels", + ) ) @property @@ -360,7 +399,10 @@ def save(self, *args, **kwargs) -> None: @lru_cache def active_projects_count(): """Return active projects count.""" - return IndexBase.get_total_count("projects", search_filters="idx_is_active:true") + return Project.objects.filter( + has_active_repositories=True, + is_active=True, + ).count() @staticmethod def bulk_save(projects: list, fields: list | None = None) -> None: # type: ignore[override] diff --git a/backend/apps/owasp/models/project_health_metrics.py b/backend/apps/owasp/models/project_health_metrics.py index 36a88c23ea..8e055cf04a 100644 --- a/backend/apps/owasp/models/project_health_metrics.py +++ b/backend/apps/owasp/models/project_health_metrics.py @@ -1,8 +1,10 @@ """Project health metrics model.""" +from functools import cached_property + from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models.functions import ExtractMonth, TruncDate +from django.db.models.functions import Coalesce, ExtractMonth, TruncDate from django.utils import timezone from apps.common.models import BulkSaveModel, TimestampedModel @@ -90,7 +92,7 @@ def age_days(self) -> int: @property def age_days_requirement(self) -> int: """Get the age requirement for the project.""" - return self.project_requirements.age_days + return self.project_requirements.age_days if self.project_requirements else 0 @property def last_commit_days(self) -> int: @@ -100,7 +102,7 @@ def last_commit_days(self) -> int: @property def last_commit_days_requirement(self) -> int: """Get the last commit requirement for the project.""" - return self.project_requirements.last_commit_days + return self.project_requirements.last_commit_days if self.project_requirements else 0 @property def last_pull_request_days(self) -> int: @@ -114,7 +116,7 @@ def last_pull_request_days(self) -> int: @property def last_pull_request_days_requirement(self) -> int: """Get the last pull request requirement for the project.""" - return self.project_requirements.last_pull_request_days + return self.project_requirements.last_pull_request_days if self.project_requirements else 0 @property def last_release_days(self) -> int: @@ -124,7 +126,7 @@ def last_release_days(self) -> int: @property def last_release_days_requirement(self) -> int: """Get the last release requirement for the project.""" - return self.project_requirements.last_release_days + return self.project_requirements.last_release_days if self.project_requirements else 0 @property def owasp_page_last_update_days(self) -> int: @@ -138,12 +140,16 @@ def owasp_page_last_update_days(self) -> int: @property def owasp_page_last_update_days_requirement(self) -> int: """Get the OWASP page last update requirement for the project.""" - return self.project_requirements.owasp_page_last_update_days + return ( + self.project_requirements.owasp_page_last_update_days + if self.project_requirements + else 0 + ) - @property - def project_requirements(self) -> ProjectHealthRequirements: + @cached_property + def project_requirements(self) -> ProjectHealthRequirements | None: """Get the project health requirements for the project's level.""" - return ProjectHealthRequirements.objects.get(level=self.project.level) + return ProjectHealthRequirements.objects.filter(level=self.project.level).first() @staticmethod def bulk_save(metrics: list, fields: list | None = None) -> None: # type: ignore[override] @@ -164,13 +170,14 @@ def get_latest_health_metrics() -> models.QuerySet["ProjectHealthMetrics"]: QuerySet[ProjectHealthMetrics]: QuerySet of project health metrics. """ + # To have a queryset that supports further filtering/ordering, + # we use a subquery to get the latest metrics per project. return ProjectHealthMetrics.objects.filter( - nest_created_at=models.Subquery( - ProjectHealthMetrics.objects.filter(project=models.OuterRef("project")) - .order_by("-nest_created_at") - .values("nest_created_at")[:1] - ), - project__is_active=True, + id__in=ProjectHealthMetrics.objects.filter(project__is_active=True) + .select_related("project") + .order_by("project_id", "-nest_created_at") + .distinct("project_id") + .values_list("id", flat=True) ) @staticmethod @@ -181,27 +188,27 @@ def get_stats() -> ProjectHealthStatsNode: ProjectHealthStatsNode: The overall health stats of all projects. """ - metrics = ProjectHealthMetrics.get_latest_health_metrics() - - projects_count_healthy = metrics.filter( - score__gte=HEALTH_SCORE_THRESHOLD_HEALTHY, - ).count() - projects_count_need_attention = metrics.filter( - score__lt=HEALTH_SCORE_THRESHOLD_HEALTHY, - score__gte=HEALTH_SCORE_THRESHOLD_NEED_ATTENTION, - ).count() - projects_count_unhealthy = metrics.filter( - score__lt=HEALTH_SCORE_THRESHOLD_NEED_ATTENTION - ).count() - - projects_count_total = metrics.count() or 1 # Avoid division by zero - - aggregation = metrics.aggregate( - average_score=models.Avg("score"), - total_contributors=models.Sum("contributors_count"), - total_forks=models.Sum("forks_count"), - total_stars=models.Sum("stars_count"), + stats = ProjectHealthMetrics.get_latest_health_metrics().aggregate( + projects_count_healthy=models.Count( + "id", filter=models.Q(score__gte=HEALTH_SCORE_THRESHOLD_HEALTHY) + ), + projects_count_need_attention=models.Count( + "id", + filter=models.Q( + score__lt=HEALTH_SCORE_THRESHOLD_HEALTHY, + score__gte=HEALTH_SCORE_THRESHOLD_NEED_ATTENTION, + ), + ), + projects_count_unhealthy=models.Count( + "id", filter=models.Q(score__lt=HEALTH_SCORE_THRESHOLD_NEED_ATTENTION) + ), + projects_count_total=models.Count("id"), + average_score=Coalesce(models.Avg("score"), 0.0), + total_contributors=Coalesce(models.Sum("contributors_count"), 0), + total_forks=Coalesce(models.Sum("forks_count"), 0), + total_stars=Coalesce(models.Sum("stars_count"), 0), ) + total = stats["projects_count_total"] or 1 # Avoid division by zero monthly_overall_metrics = ( ProjectHealthMetrics.objects.annotate(month=ExtractMonth("nest_created_at")) .filter( @@ -214,22 +221,26 @@ def get_stats() -> ProjectHealthStatsNode: score=models.Avg("score"), ) ) + months = [] + scores = [] + for entry in monthly_overall_metrics: + months.append(entry["month"]) + scores.append(entry["score"]) + return ProjectHealthStatsNode( - average_score=aggregation.get("average_score", 0.0), + average_score=stats["average_score"], # We use all metrics instead of latest metrics to get the monthly trend - monthly_overall_scores=list(monthly_overall_metrics.values_list("score", flat=True)), - monthly_overall_scores_months=list( - monthly_overall_metrics.values_list("month", flat=True) - ), - projects_count_healthy=projects_count_healthy, - projects_count_need_attention=projects_count_need_attention, - projects_count_unhealthy=projects_count_unhealthy, - projects_percentage_healthy=(projects_count_healthy / projects_count_total) * 100, + monthly_overall_scores=scores, + monthly_overall_scores_months=months, + projects_count_healthy=stats["projects_count_healthy"], + projects_count_need_attention=stats["projects_count_need_attention"], + projects_count_unhealthy=stats["projects_count_unhealthy"], + projects_percentage_healthy=(stats["projects_count_healthy"] / total) * 100, projects_percentage_need_attention=( - (projects_count_need_attention / projects_count_total) * 100 + (stats["projects_count_need_attention"] / total) * 100 ), - projects_percentage_unhealthy=(projects_count_unhealthy / projects_count_total) * 100, - total_contributors=(aggregation.get("total_contributors", 0)), - total_forks=(aggregation.get("total_forks", 0)), - total_stars=(aggregation.get("total_stars", 0)), + projects_percentage_unhealthy=(stats["projects_count_unhealthy"] / total) * 100, + total_contributors=(stats["total_contributors"]), + total_forks=(stats["total_forks"]), + total_stars=(stats["total_stars"]), ) diff --git a/backend/apps/owasp/models/snapshot.py b/backend/apps/owasp/models/snapshot.py index f71f43bd22..960130a73d 100644 --- a/backend/apps/owasp/models/snapshot.py +++ b/backend/apps/owasp/models/snapshot.py @@ -11,6 +11,11 @@ class Meta: db_table = "owasp_snapshots" verbose_name_plural = "Snapshots" + indexes = [ + models.Index(fields=["-created_at"], name="owasp_snapshot_created_at_idx"), + models.Index(fields=["key", "status"], name="owasp_snapshot_key_status_idx"), + ] + class Status(models.TextChoices): PENDING = "pending", "Pending" PROCESSING = "processing", "Processing" diff --git a/backend/data/nest.dump b/backend/data/nest.dump new file mode 100644 index 0000000000..629fecd4b1 Binary files /dev/null and b/backend/data/nest.dump differ diff --git a/backend/entrypoint.fuzz.sh b/backend/entrypoint.fuzz.sh new file mode 100644 index 0000000000..bbfbab7c52 --- /dev/null +++ b/backend/entrypoint.fuzz.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +set -e + +if [ -z "$BASE_URL" ]; then + echo "Error: BASE_URL environment variable is not set." >&2 + exit 1 +fi + +echo "Fetching CSRF token..." +CSRF_TOKEN=$(curl -fsSL "$BASE_URL/csrf" | jq -r '.csrftoken') + +if [ -z "$CSRF_TOKEN" ] || [ "$CSRF_TOKEN" = "null" ]; then + echo "Error: Failed to retrieve CSRF token." >&2 + exit 1 +fi + +export CSRF_TOKEN + +echo "CSRF token retrieved successfully." +:> ./schemathesis.toml + +# Number of examples to generate per endpoint +# See https://schemathesis.readthedocs.io/en/stable/explanations/data-generation/#how-many-test-cases-does-schemathesis-generate + +echo "generation.max-examples = 100" >> ./schemathesis.toml + +# Enable specific checks +# See https://schemathesis.readthedocs.io/en/stable/reference/checks/ +# Schemathesis raises errors for bad requests, so we need to explicitly enable the checks we want +echo "[checks]" >> ./schemathesis.toml +echo "enabled = false" >> ./schemathesis.toml +echo "response_schema_conformance.enabled = true" >> ./schemathesis.toml +echo "not_a_server_error.enabled = true" >> ./schemathesis.toml + +if [ -n "$TEST_FILE" ]; then + echo "Using test file: $TEST_FILE" +else + echo "Error: TEST_FILE environment variable is not set." >&2 + exit 1 +fi + +echo "Starting fuzzing process..." + +if [ -n "$CI" ]; then + pytest ./tests/${TEST_FILE} +else + pytest --log-cli-level=INFO -s ./tests/${TEST_FILE} +fi diff --git a/backend/poetry.lock b/backend/poetry.lock index 88d171cfb7..6b3b6411d9 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -306,18 +306,18 @@ wrapt = "*" [[package]] name = "boto3" -version = "1.42.29" +version = "1.42.30" description = "The AWS SDK for Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "boto3-1.42.29-py3-none-any.whl", hash = "sha256:6c9c4dece67bf72d82ba7dff48e33a56a87cdf9b16c8887f88ca7789a95d3317"}, - {file = "boto3-1.42.29.tar.gz", hash = "sha256:247e54f24116ad6792cfc14b274288383af3ec3433b0547da8a14a8bd6e81950"}, + {file = "boto3-1.42.30-py3-none-any.whl", hash = "sha256:d7e548bea65e0ae2c465c77de937bc686b591aee6a352d5a19a16bc751e591c1"}, + {file = "boto3-1.42.30.tar.gz", hash = "sha256:ba9cd2f7819637d15bfbeb63af4c567fcc8a7dcd7b93dd12734ec58601169538"}, ] [package.dependencies] -botocore = ">=1.42.29,<1.43.0" +botocore = ">=1.42.30,<1.43.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.16.0,<0.17.0" @@ -326,14 +326,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.42.29" +version = "1.42.30" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "botocore-1.42.29-py3-none-any.whl", hash = "sha256:b45f8dfc1de5106a9d040c5612f267582e68b2b2c5237477dff85c707c1c5d11"}, - {file = "botocore-1.42.29.tar.gz", hash = "sha256:0fe869227a1dfe818f691a31b8c1693e39be8056a6dff5d6d4b3fc5b3a5e7d42"}, + {file = "botocore-1.42.30-py3-none-any.whl", hash = "sha256:97070a438cac92430bb7b65f8ebd7075224f4a289719da4ee293d22d1e98db02"}, + {file = "botocore-1.42.30.tar.gz", hash = "sha256:9bf1662b8273d5cc3828a49f71ca85abf4e021011c1f0a71f41a2ea5769a5116"}, ] [package.dependencies] @@ -1277,14 +1277,14 @@ dev = ["mypy (>=1.15)"] [[package]] name = "elevenlabs" -version = "2.30.0" +version = "2.31.0" description = "" optional = false python-versions = "<4.0,>=3.8" groups = ["video"] files = [ - {file = "elevenlabs-2.30.0-py3-none-any.whl", hash = "sha256:eeee92703a27e7ecd0e8ba4d547f730bcef4ecb02485efa59aeecab3a904d024"}, - {file = "elevenlabs-2.30.0.tar.gz", hash = "sha256:a6a0474e045b93475fcd5f5829b67438d5a6aef9698b6f8758e7148ac03c2b12"}, + {file = "elevenlabs-2.31.0-py3-none-any.whl", hash = "sha256:163062ab3c8da274a8fa43887974658e0b670b81ab887fbf53579c29e90dd433"}, + {file = "elevenlabs-2.31.0.tar.gz", hash = "sha256:709eca626b67e19b379a099f46af608786f269c3d8824ff1adc04749f7d8b23b"}, ] [package.dependencies] @@ -2976,60 +2976,61 @@ files = [ [[package]] name = "ormsgpack" -version = "1.12.1" +version = "1.12.2" description = "Fast, correct Python msgpack library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "ormsgpack-1.12.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:62e3614cab63fa5aa42f5f0ca3cd12899f0bfc5eb8a5a0ebab09d571c89d427d"}, - {file = "ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:86d9fbf85c05c69c33c229d2eba7c8c3500a56596cd8348131c918acd040d6af"}, - {file = "ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8d246e66f09d8e0f96e770829149ee83206e90ed12f5987998bb7be84aec99fe"}, - {file = "ormsgpack-1.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfc2c830a1ed2d00de713d08c9e62efa699e8fd29beafa626aaebe466f583ebb"}, - {file = "ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc892757d8f9eea5208268a527cf93c98409802f6a9f7c8d71a7b8f9ba5cb944"}, - {file = "ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0de1dbcf11ea739ac4a882b43d5c2055e6d99ce64e8d6502e25d6d881700c017"}, - {file = "ormsgpack-1.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d5065dfb9ec4db93241c60847624d9aeef4ccb449c26a018c216b55c69be83c0"}, - {file = "ormsgpack-1.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d17103c4726181d7000c61b751c881f1b6f401d146df12da028fc730227df19"}, - {file = "ormsgpack-1.12.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4038f59ae0e19dac5e5d9aae4ec17ff84a79e046342ee73ccdecf3547ecf0d34"}, - {file = "ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16c63b0c5a3eec467e4bb33a14dabba076b7d934dff62898297b5c0b5f7c3cb3"}, - {file = "ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:74fd6a8e037eb310dda865298e8d122540af00fe5658ec18b97a1d34f4012e4d"}, - {file = "ormsgpack-1.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ad60308e233dd824a1859eabb5fe092e123e885eafa4ad5789322329c80fb5"}, - {file = "ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:35127464c941c1219acbe1a220e48d55e7933373d12257202f4042f7044b4c90"}, - {file = "ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c48d1c50794692d1e6e3f8c3bb65f5c3acfaae9347e506484a65d60b3d91fb50"}, - {file = "ormsgpack-1.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b512b2ad6feaaefdc26e05431ed2843e42483041e354e167c53401afaa83d919"}, - {file = "ormsgpack-1.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:93f30db95e101a9616323bfc50807ad00e7f6197cea2216d2d24af42afc77d88"}, - {file = "ormsgpack-1.12.1-cp311-cp311-win_arm64.whl", hash = "sha256:d75b5fa14f6abffce2c392ee03b4731199d8a964c81ee8645c4c79af0e80fd50"}, - {file = "ormsgpack-1.12.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4d7fb0e1b6fbc701d75269f7405a4f79230a6ce0063fb1092e4f6577e312f86d"}, - {file = "ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:43a9353e2db5b024c91a47d864ef15eaa62d81824cfc7740fed4cef7db738694"}, - {file = "ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fc8fe866b7706fc25af0adf1f600bc06ece5b15ca44e34641327198b821e5c3c"}, - {file = "ormsgpack-1.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813755b5f598a78242042e05dfd1ada4e769e94b98c9ab82554550f97ff4d641"}, - {file = "ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8eea2a13536fae45d78f93f2cc846c9765c7160c85f19cfefecc20873c137cdd"}, - {file = "ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7a02ebda1a863cbc604740e76faca8eee1add322db2dcbe6cf32669fffdff65c"}, - {file = "ormsgpack-1.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c0bd63897c439931cdf29348e5e6e8c330d529830e848d10767615c0f3d1b82"}, - {file = "ormsgpack-1.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:362f2e812f8d7035dc25a009171e09d7cc97cb30d3c9e75a16aeae00ca3c1dcf"}, - {file = "ormsgpack-1.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:6190281e381db2ed0045052208f47a995ccf61eed48f1215ae3cce3fbccd59c5"}, - {file = "ormsgpack-1.12.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9663d6b3ecc917c063d61a99169ce196a80f3852e541ae404206836749459279"}, - {file = "ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32e85cfbaf01a94a92520e7fe7851cfcfe21a5698299c28ab86194895f9b9233"}, - {file = "ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dabfd2c24b59c7c69870a5ecee480dfae914a42a0c2e7c9d971cf531e2ba471a"}, - {file = "ormsgpack-1.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51bbf2b64afeded34ccd8e25402e4bca038757913931fa0d693078d75563f6f9"}, - {file = "ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9959a71dde1bd0ced84af17facc06a8afada495a34e9cb1bad8e9b20d4c59cef"}, - {file = "ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:e9be0e3b62d758f21f5b20e0e06b3a240ec546c4a327bf771f5825462aa74714"}, - {file = "ormsgpack-1.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a29d49ab7fdd77ea787818e60cb4ef491708105b9c4c9b0f919201625eb036b5"}, - {file = "ormsgpack-1.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:c418390b47a1d367e803f6c187f77e4d67c7ae07ba962e3a4a019001f4b0291a"}, - {file = "ormsgpack-1.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:cfa22c91cffc10a7fbd43729baff2de7d9c28cef2509085a704168ae31f02568"}, - {file = "ormsgpack-1.12.1-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b93c91efb1a70751a1902a5b43b27bd8fd38e0ca0365cf2cde2716423c15c3a6"}, - {file = "ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf0ea0389167b5fa8d2933dd3f33e887ec4ba68f89c25214d7eec4afd746d22"}, - {file = "ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4c29af837f35af3375070689e781161e7cf019eb2f7cd641734ae45cd001c0d"}, - {file = "ormsgpack-1.12.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336fc65aa0fe65896a3dabaae31e332a0a98b4a00ad7b0afde21a7505fd23ff3"}, - {file = "ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:940f60aabfefe71dd6b82cb33f4ff10b2e7f5fcfa5f103cdb0a23b6aae4c713c"}, - {file = "ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:596ad9e1b6d4c95595c54aaf49b1392609ca68f562ce06f4f74a5bc4053bcda4"}, - {file = "ormsgpack-1.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:575210e8fcbc7b0375026ba040a5eef223e9f66a4453d9623fc23282ae09c3c8"}, - {file = "ormsgpack-1.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:647daa3718572280893456be44c60aea6690b7f2edc54c55648ee66e8f06550f"}, - {file = "ormsgpack-1.12.1-cp314-cp314-win_arm64.whl", hash = "sha256:a8b3ab762a6deaf1b6490ab46dda0c51528cf8037e0246c40875c6fe9e37b699"}, - {file = "ormsgpack-1.12.1-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:12087214e436c1f6c28491949571abea759a63111908c4f7266586d78144d7a8"}, - {file = "ormsgpack-1.12.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6d54c14cf86ef13f10ccade94d1e7de146aa9b17d371e18b16e95f329393b7"}, - {file = "ormsgpack-1.12.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f3584d07882b7ea2a1a589f795a3af97fe4c2932b739408e6d1d9d286cad862"}, - {file = "ormsgpack-1.12.1.tar.gz", hash = "sha256:a3877fde1e4f27a39f92681a0aab6385af3a41d0c25375d33590ae20410ea2ac"}, + {file = "ormsgpack-1.12.2-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c1429217f8f4d7fcb053523bbbac6bed5e981af0b85ba616e6df7cce53c19657"}, + {file = "ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f13034dc6c84a6280c6c33db7ac420253852ea233fc3ee27c8875f8dd651163"}, + {file = "ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59f5da97000c12bc2d50e988bdc8576b21f6ab4e608489879d35b2c07a8ab51a"}, + {file = "ormsgpack-1.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e4459c3f27066beadb2b81ea48a076a417aafffff7df1d3c11c519190ed44f2"}, + {file = "ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a1c460655d7288407ffa09065e322a7231997c0d62ce914bf3a96ad2dc6dedd"}, + {file = "ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:458e4568be13d311ef7d8877275e7ccbe06c0e01b39baaac874caaa0f46d826c"}, + {file = "ormsgpack-1.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8cde5eaa6c6cbc8622db71e4a23de56828e3d876aeb6460ffbcb5b8aff91093b"}, + {file = "ormsgpack-1.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:dc7a33be14c347893edbb1ceda89afbf14c467d593a5ee92c11de4f1666b4d4f"}, + {file = "ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9"}, + {file = "ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a"}, + {file = "ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5"}, + {file = "ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181"}, + {file = "ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b"}, + {file = "ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92"}, + {file = "ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a"}, + {file = "ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c"}, + {file = "ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd"}, + {file = "ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7"}, + {file = "ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d"}, + {file = "ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e"}, + {file = "ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc"}, + {file = "ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e"}, + {file = "ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6"}, + {file = "ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd"}, + {file = "ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4"}, + {file = "ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6"}, + {file = "ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355"}, + {file = "ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1"}, + {file = "ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172"}, + {file = "ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d"}, + {file = "ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7"}, + {file = "ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685"}, + {file = "ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258"}, + {file = "ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9"}, + {file = "ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709"}, + {file = "ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c"}, + {file = "ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553"}, + {file = "ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13"}, + {file = "ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d"}, + {file = "ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede"}, + {file = "ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e"}, + {file = "ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285"}, + {file = "ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f"}, + {file = "ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c"}, + {file = "ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8"}, + {file = "ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033"}, + {file = "ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d"}, + {file = "ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2"}, + {file = "ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33"}, ] [[package]] diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 33df5f8306..0dfe0eaf6b 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -104,6 +104,10 @@ lint.per-file-ignores."**/management/commands/*.py" = [ "SLF001", # https://docs.astral.sh/ruff/rules/private-member-access/ "T201", # https://docs.astral.sh/ruff/rules/print/ ] +lint.per-file-ignores."**/management/commands/dump_data.py" = [ + "S603", # https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true/, + "S607", # https://docs.astral.sh/ruff/rules/start-process-with-partial-path/, +] lint.per-file-ignores."**/migrations/*.py" = [ "D100", # https://docs.astral.sh/ruff/rules/undocumented-public-module/ "D101", # https://docs.astral.sh/ruff/rules/undocumented-public-class/ diff --git a/backend/settings/base.py b/backend/settings/base.py index b919df4c76..3fb23a4375 100644 --- a/backend/settings/base.py +++ b/backend/settings/base.py @@ -19,7 +19,9 @@ class Base(Configuration): DEBUG = False GITHUB_APP_ID = None GITHUB_APP_INSTALLATION_ID = None + IS_E2E_ENVIRONMENT = False IS_LOCAL_ENVIRONMENT = False + IS_FUZZ_ENVIRONMENT = False IS_PRODUCTION_ENVIRONMENT = False IS_STAGING_ENVIRONMENT = False IS_TEST_ENVIRONMENT = False @@ -91,6 +93,7 @@ class Base(Configuration): "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", + "apps.common.middlewares.block_null_characters.BlockNullCharactersMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", ] @@ -137,10 +140,14 @@ class Base(Configuration): REDIS_HOST = values.SecretValue(environ_name="REDIS_HOST") REDIS_PASSWORD = values.SecretValue(environ_name="REDIS_PASSWORD") + REDIS_AUTH_ENABLED = values.BooleanValue(environ_name="REDIS_AUTH_ENABLED", default=True) CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": f"rediss://:{REDIS_PASSWORD}@{REDIS_HOST}:6379", + "LOCATION": f"rediss://:{REDIS_PASSWORD}@{REDIS_HOST}:6379" + # GH actions does not support authenticated redis connections. + if REDIS_AUTH_ENABLED + else f"redis://{REDIS_HOST}:6379", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, diff --git a/backend/settings/e2e.py b/backend/settings/e2e.py new file mode 100644 index 0000000000..d70e0fc995 --- /dev/null +++ b/backend/settings/e2e.py @@ -0,0 +1,24 @@ +"""OWASP Nest end-to-end testing configuration.""" + +from configurations import values + +from settings.base import Base + + +class E2E(Base): + """End-to-end testing configuration.""" + + APP_NAME = "OWASP Nest E2E Testing" + SITE_URL = "http://localhost:9000" + + ALLOWED_ORIGINS = ( + "http://frontend:3000", # NOSONAR + "http://localhost:3000", + ) + + CORS_ALLOWED_ORIGINS = ALLOWED_ORIGINS + CSRF_TRUSTED_ORIGINS = ALLOWED_ORIGINS + + IS_E2E_ENVIRONMENT = True + LOGGING = {} + PUBLIC_IP_ADDRESS = values.Value() diff --git a/backend/settings/fuzz.py b/backend/settings/fuzz.py new file mode 100644 index 0000000000..11d3471b59 --- /dev/null +++ b/backend/settings/fuzz.py @@ -0,0 +1,16 @@ +"""OWASP Nest fuzz testing configuration.""" + +from configurations import values + +from settings.base import Base + + +class Fuzz(Base): + """Fuzz testing configuration.""" + + APP_NAME = "OWASP Nest Fuzz Testing" + SITE_URL = "http://localhost:9500" + + IS_FUZZ_ENVIRONMENT = True + LOGGING = {} + PUBLIC_IP_ADDRESS = values.Value() diff --git a/backend/settings/graphql.py b/backend/settings/graphql.py index edb830c1ff..6020741d74 100644 --- a/backend/settings/graphql.py +++ b/backend/settings/graphql.py @@ -1,6 +1,8 @@ """GraphQL schema.""" import strawberry +from strawberry.extensions import QueryDepthLimiter +from strawberry_django.optimizer import DjangoOptimizerExtension from apps.api.internal.mutations import ApiMutations from apps.api.internal.queries import ApiKeyQueries @@ -41,4 +43,8 @@ class Query( """Schema queries.""" -schema = strawberry.Schema(mutation=Mutation, query=Query, extensions=[CacheExtension]) +schema = strawberry.Schema( + mutation=Mutation, + query=Query, + extensions=[CacheExtension, QueryDepthLimiter(max_depth=5), DjangoOptimizerExtension()], +) diff --git a/backend/tests/apps/api/rest/v0/event_test.py b/backend/tests/apps/api/rest/v0/event_test.py index 0ef10beb15..fd2cc2e507 100644 --- a/backend/tests/apps/api/rest/v0/event_test.py +++ b/backend/tests/apps/api/rest/v0/event_test.py @@ -1,43 +1,48 @@ from datetime import datetime import pytest +from django.utils import timezone from apps.api.rest.v0.event import EventDetail +from apps.owasp.models.event import Event as EventModel + +current_timezone = timezone.get_current_timezone() @pytest.mark.parametrize( - "event_data", + "event_object", [ - { - "description": "A test event", - "end_date": "2025-03-14T00:00:00Z", - "key": "test-event", - "latitude": 37.7749, - "longitude": -122.4194, - "name": "Test Event", - "start_date": "2025-03-14T00:00:00Z", - "url": "https://github.com/owasp/Nest", - }, - { - "description": "this is a biggest event", - "end_date": "2023-05-18T00:00:00Z", - "key": "biggest-event", - "latitude": 59.9139, - "longitude": 10.7522, - "name": "biggest event", - "start_date": "2022-05-19T00:00:00Z", - "url": "https://github.com/owasp", - }, + EventModel( + description="this is a sample event", + end_date=datetime(2023, 6, 15, tzinfo=current_timezone).date(), + key="sample-event", + latitude=59.9139, + longitude=10.7522, + name="sample event", + start_date=datetime(2023, 6, 14, tzinfo=current_timezone).date(), + url="https://github.com/owasp/Nest", + ), + EventModel( + description=None, + end_date=None, + key="event-without-end-date", + latitude=None, + longitude=None, + name="event without end date", + start_date=datetime(2023, 7, 1, tzinfo=current_timezone).date(), + url=None, + ), ], ) -def test_event_serializer_validation(event_data): - event = EventDetail(**event_data) +def test_event_serializer_validation(event_object: EventModel): + event = EventDetail.from_orm(event_object) - assert event.description == event_data["description"] - assert event.end_date == datetime.fromisoformat(event_data["end_date"]) - assert event.key == event_data["key"] - assert event.latitude == event_data["latitude"] - assert event.longitude == event_data["longitude"] - assert event.name == event_data["name"] - assert event.start_date == datetime.fromisoformat(event_data["start_date"]) - assert event.url == event_data["url"] + assert event.description == event_object.description + end_date = event_object.end_date.isoformat() if event_object.end_date else None + assert event.end_date == end_date + assert event.key == event_object.key + assert event.latitude == event_object.latitude + assert event.longitude == event_object.longitude + assert event.name == event_object.name + assert event.start_date == event_object.start_date.isoformat() + assert event.url == event_object.url diff --git a/backend/tests/apps/common/graphql_node_base_test.py b/backend/tests/apps/common/graphql_node_base_test.py new file mode 100644 index 0000000000..ee24df72c1 --- /dev/null +++ b/backend/tests/apps/common/graphql_node_base_test.py @@ -0,0 +1,10 @@ +"""Base Test Class for GraphQL Node Tests.""" + + +class GraphQLNodeBaseTest: + """Base Test Class for GraphQL Node Tests.""" + + def _get_field_by_name(self, name, node_class): + return next( + (f for f in node_class.__strawberry_definition__.fields if f.name == name), None + ) diff --git a/backend/tests/apps/common/management/commands/dump_data_test.py b/backend/tests/apps/common/management/commands/dump_data_test.py new file mode 100644 index 0000000000..06bba46cf5 --- /dev/null +++ b/backend/tests/apps/common/management/commands/dump_data_test.py @@ -0,0 +1,100 @@ +from unittest.mock import MagicMock, patch + +from django.core.management import call_command +from django.test import override_settings +from psycopg2 import sql + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": "db-name", + "USER": "db-user", + "PASSWORD": "db-pass", # NOSONAR + "HOST": "db-host", + "PORT": "5432", + } +} + + +class TestDumpDataCommand: + @override_settings(DATABASES=DATABASES) + @patch("apps.common.management.commands.dump_data.run") + @patch("apps.common.management.commands.dump_data.connect") + @patch("apps.common.management.commands.dump_data.Path") + def test_dump_data(self, mock_path, mock_connect, mock_run): + # Mock psycopg2 connection/cursor + mock_conn = MagicMock() + mock_cursor = MagicMock() + mock_connect.return_value = mock_conn + mock_conn.cursor.return_value.__enter__.return_value = mock_cursor + mock_cursor.fetchall.return_value = [("public.users",), ("public.members",)] + mock_resolve = MagicMock() + mock_path.return_value.resolve.return_value = mock_resolve + call_command( + "dump_data", + "--output", + "data/dump.dump", + ) + + # Verify temp DB created from template + expected_temp_db = "temp_db-name" + mock_connect.assert_any_call( + dbname="postgres", + user="db-user", + # ruff: noqa: S106 + password="db-pass", # NOSONAR + host="db-host", + port="5432", + ) + mock_cursor.execute.assert_any_call( + sql.SQL("CREATE DATABASE {temp_db} TEMPLATE {DB_NAME};").format( + temp_db=sql.Identifier(expected_temp_db), + DB_NAME=sql.Identifier("db-name"), + ) + ) + executed_sql = [str(c.args[0]) for c in mock_cursor.execute.call_args_list] + assert ( + str( + sql.SQL( + """ + SELECT table_name + FROM information_schema.columns + WHERE table_schema = 'public' AND column_name = 'email'; + """ + ) + ) + in executed_sql + ) + + assert [ + "pg_dump", + "-h", + "db-host", + "-p", + "5432", + "-U", + "db-user", + "-d", + expected_temp_db, + "--compress=9", + "--data-only", + "--no-owner", + "--no-privileges", + "--format=custom", + "--table=public.owasp_*", + "--table=public.github_*", + "--table=public.slack_members", + "--table=public.slack_workspaces", + "--table=public.slack_conversations", + "--table=public.slack_messages", + "-f", + str(mock_resolve), + ] == mock_run.call_args[0][0] + # Ensure DROP DATABASE executed at the end + mock_cursor.execute.assert_any_call( + sql.SQL("DROP DATABASE IF EXISTS {temp_db};").format( + temp_db=sql.Identifier(expected_temp_db) + ) + ) + mock_path.return_value.resolve.assert_called_once() + mock_resolve.parent.mkdir.assert_called_once_with(parents=True, exist_ok=True) diff --git a/backend/tests/apps/github/api/internal/nodes/issue_test.py b/backend/tests/apps/github/api/internal/nodes/issue_test.py index b3d4a2dbd9..54a6f61f62 100644 --- a/backend/tests/apps/github/api/internal/nodes/issue_test.py +++ b/backend/tests/apps/github/api/internal/nodes/issue_test.py @@ -3,9 +3,10 @@ from unittest.mock import Mock from apps.github.api.internal.nodes.issue import IssueNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestIssueNode: +class TestIssueNode(GraphQLNodeBaseTest): """Test cases for IssueNode class.""" def test_issue_node_type(self): @@ -32,15 +33,6 @@ def test_issue_node_fields(self): } assert field_names == expected_field_names - def test_author_field(self): - """Test author field resolution.""" - mock_issue = Mock() - mock_author = Mock() - mock_issue.author = mock_author - - result = IssueNode.author(mock_issue) - assert result == mock_author - def test_organization_name_with_organization(self): """Test organization_name field when organization exists.""" mock_issue = Mock() @@ -50,7 +42,8 @@ def test_organization_name_with_organization(self): mock_repository.organization = mock_organization mock_issue.repository = mock_repository - result = IssueNode.organization_name(mock_issue) + field = self._get_field_by_name("organization_name", IssueNode) + result = field.base_resolver.wrapped_func(None, mock_issue) assert result == "test-org" def test_organization_name_without_organization(self): @@ -60,7 +53,8 @@ def test_organization_name_without_organization(self): mock_repository.organization = None mock_issue.repository = mock_repository - result = IssueNode.organization_name(mock_issue) + field = self._get_field_by_name("organization_name", IssueNode) + result = field.base_resolver.wrapped_func(None, mock_issue) assert result is None def test_organization_name_without_repository(self): @@ -68,7 +62,8 @@ def test_organization_name_without_repository(self): mock_issue = Mock() mock_issue.repository = None - result = IssueNode.organization_name(mock_issue) + field = self._get_field_by_name("organization_name", IssueNode) + result = field.base_resolver.wrapped_func(None, mock_issue) assert result is None def test_repository_name_with_repository(self): @@ -78,7 +73,8 @@ def test_repository_name_with_repository(self): mock_repository.name = "test-repo" mock_issue.repository = mock_repository - result = IssueNode.repository_name(mock_issue) + field = self._get_field_by_name("repository_name", IssueNode) + result = field.base_resolver.wrapped_func(None, mock_issue) assert result == "test-repo" def test_repository_name_without_repository(self): @@ -86,5 +82,6 @@ def test_repository_name_without_repository(self): mock_issue = Mock() mock_issue.repository = None - result = IssueNode.repository_name(mock_issue) + field = self._get_field_by_name("repository_name", IssueNode) + result = field.base_resolver.wrapped_func(None, mock_issue) assert result is None diff --git a/backend/tests/apps/github/api/internal/nodes/milestone_test.py b/backend/tests/apps/github/api/internal/nodes/milestone_test.py index a143149c98..7d8dd7567f 100644 --- a/backend/tests/apps/github/api/internal/nodes/milestone_test.py +++ b/backend/tests/apps/github/api/internal/nodes/milestone_test.py @@ -4,9 +4,10 @@ from unittest.mock import Mock from apps.github.api.internal.nodes.milestone import MilestoneNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestMilestoneNode: +class TestMilestoneNode(GraphQLNodeBaseTest): """Test cases for MilestoneNode class.""" def test_milestone_node_type(self): @@ -30,15 +31,6 @@ def test_milestone_node_fields(self): } assert field_names == expected_field_names - def test_author_field(self): - """Test author field resolution.""" - mock_milestone = Mock() - mock_author = Mock() - mock_milestone.author = mock_author - - result = MilestoneNode.author(mock_milestone) - assert result == mock_author - def test_organization_name_with_organization(self): """Test organization_name field when organization exists.""" mock_milestone = Mock() @@ -48,7 +40,8 @@ def test_organization_name_with_organization(self): mock_repository.organization = mock_organization mock_milestone.repository = mock_repository - result = MilestoneNode.organization_name(mock_milestone) + field = self._get_field_by_name("organization_name", MilestoneNode) + result = field.base_resolver.wrapped_func(None, mock_milestone) assert result == "test-org" def test_organization_name_without_organization(self): @@ -58,7 +51,8 @@ def test_organization_name_without_organization(self): mock_repository.organization = None mock_milestone.repository = mock_repository - result = MilestoneNode.organization_name(mock_milestone) + field = self._get_field_by_name("organization_name", MilestoneNode) + result = field.base_resolver.wrapped_func(None, mock_milestone) assert result is None def test_organization_name_without_repository(self): @@ -66,7 +60,8 @@ def test_organization_name_without_repository(self): mock_milestone = Mock() mock_milestone.repository = None - result = MilestoneNode.organization_name(mock_milestone) + field = self._get_field_by_name("organization_name", MilestoneNode) + result = field.base_resolver.wrapped_func(None, mock_milestone) assert result is None def test_progress_with_issues(self): @@ -75,7 +70,8 @@ def test_progress_with_issues(self): mock_milestone.closed_issues_count = 7 mock_milestone.open_issues_count = 3 - result = MilestoneNode.progress(mock_milestone) + field = self._get_field_by_name("progress", MilestoneNode) + result = field.base_resolver.wrapped_func(None, mock_milestone) assert math.isclose(result, 70.0) def test_progress_without_issues(self): @@ -84,7 +80,8 @@ def test_progress_without_issues(self): mock_milestone.closed_issues_count = 0 mock_milestone.open_issues_count = 0 - result = MilestoneNode.progress(mock_milestone) + field = self._get_field_by_name("progress", MilestoneNode) + result = field.base_resolver.wrapped_func(None, mock_milestone) assert math.isclose(result, 0.0) def test_repository_name_with_repository(self): @@ -94,7 +91,8 @@ def test_repository_name_with_repository(self): mock_repository.name = "test-repo" mock_milestone.repository = mock_repository - result = MilestoneNode.repository_name(mock_milestone) + field = self._get_field_by_name("repository_name", MilestoneNode) + result = field.base_resolver.wrapped_func(None, mock_milestone) assert result == "test-repo" def test_repository_name_without_repository(self): @@ -102,5 +100,6 @@ def test_repository_name_without_repository(self): mock_milestone = Mock() mock_milestone.repository = None - result = MilestoneNode.repository_name(mock_milestone) + field = self._get_field_by_name("repository_name", MilestoneNode) + result = field.base_resolver.wrapped_func(None, mock_milestone) assert result is None diff --git a/backend/tests/apps/github/api/internal/nodes/organization_test.py b/backend/tests/apps/github/api/internal/nodes/organization_test.py index 10bc2804a4..cccbb65282 100644 --- a/backend/tests/apps/github/api/internal/nodes/organization_test.py +++ b/backend/tests/apps/github/api/internal/nodes/organization_test.py @@ -6,9 +6,10 @@ OrganizationNode, OrganizationStatsNode, ) +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestOrganizationNode: +class TestOrganizationNode(GraphQLNodeBaseTest): def test_organization_node_inheritance(self): assert hasattr(OrganizationNode, "__strawberry_definition__") @@ -70,7 +71,8 @@ def test_stats_method_with_data(self, mock_repo_contributor_objects, mock_reposi mock_organization = Mock() # Call stats method - result = OrganizationNode.stats(mock_organization) + field = self._get_field_by_name("stats", OrganizationNode) + result = field.base_resolver.wrapped_func(None, mock_organization) # Verify result assert isinstance(result, OrganizationStatsNode) @@ -105,7 +107,8 @@ def test_stats_method_with_none_values( mock_organization = Mock() # Call stats method - result = OrganizationNode.stats(mock_organization) + field = self._get_field_by_name("stats", OrganizationNode) + result = field.base_resolver.wrapped_func(None, mock_organization) # Verify result with default values assert isinstance(result, OrganizationStatsNode) @@ -120,11 +123,12 @@ def test_url_method(self): mock_organization = Mock() mock_organization.url = "https://github.com/test-org" - result = OrganizationNode.url(mock_organization) + field = self._get_field_by_name("url", OrganizationNode) + result = field.base_resolver.wrapped_func(None, mock_organization) assert result == "https://github.com/test-org" -class TestOrganizationStatsNode: +class TestOrganizationStatsNode(GraphQLNodeBaseTest): def test_organization_stats_node(self): expected_fields = { "total_contributors", diff --git a/backend/tests/apps/github/api/internal/nodes/pull_request_test.py b/backend/tests/apps/github/api/internal/nodes/pull_request_test.py index e5cad0f0a0..d0515d7e5e 100644 --- a/backend/tests/apps/github/api/internal/nodes/pull_request_test.py +++ b/backend/tests/apps/github/api/internal/nodes/pull_request_test.py @@ -3,9 +3,10 @@ from unittest.mock import Mock from apps.github.api.internal.nodes.pull_request import PullRequestNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestPullRequestNode: +class TestPullRequestNode(GraphQLNodeBaseTest): """Test cases for PullRequestNode class.""" def test_pull_request_node_type(self): @@ -26,15 +27,6 @@ def test_meta_configuration(self): } assert field_names == expected_field_names - def test_author_field(self): - """Test author field resolution.""" - mock_pr = Mock() - mock_author = Mock() - mock_pr.author = mock_author - - result = PullRequestNode.author(mock_pr) - assert result == mock_author - def test_organization_name_with_organization(self): """Test organization_name field when organization exists.""" mock_pr = Mock() @@ -44,7 +36,8 @@ def test_organization_name_with_organization(self): mock_repository.organization = mock_organization mock_pr.repository = mock_repository - result = PullRequestNode.organization_name(mock_pr) + field = self._get_field_by_name("organization_name", PullRequestNode) + result = field.base_resolver.wrapped_func(None, mock_pr) assert result == "test-org" def test_organization_name_without_organization(self): @@ -54,7 +47,8 @@ def test_organization_name_without_organization(self): mock_repository.organization = None mock_pr.repository = mock_repository - result = PullRequestNode.organization_name(mock_pr) + field = self._get_field_by_name("organization_name", PullRequestNode) + result = field.base_resolver.wrapped_func(None, mock_pr) assert result is None def test_organization_name_without_repository(self): @@ -62,7 +56,8 @@ def test_organization_name_without_repository(self): mock_pr = Mock() mock_pr.repository = None - result = PullRequestNode.organization_name(mock_pr) + field = self._get_field_by_name("organization_name", PullRequestNode) + result = field.base_resolver.wrapped_func(None, mock_pr) assert result is None def test_repository_name_with_repository(self): @@ -72,7 +67,8 @@ def test_repository_name_with_repository(self): mock_repository.name = "test-repo" mock_pr.repository = mock_repository - result = PullRequestNode.repository_name(mock_pr) + field = self._get_field_by_name("repository_name", PullRequestNode) + result = field.base_resolver.wrapped_func(None, mock_pr) assert result == "test-repo" def test_repository_name_without_repository(self): @@ -80,7 +76,8 @@ def test_repository_name_without_repository(self): mock_pr = Mock() mock_pr.repository = None - result = PullRequestNode.repository_name(mock_pr) + field = self._get_field_by_name("repository_name", PullRequestNode) + result = field.base_resolver.wrapped_func(None, mock_pr) assert result is None def test_url_with_url(self): @@ -88,13 +85,6 @@ def test_url_with_url(self): mock_pr = Mock() mock_pr.url = "https://github.com/test-org/test-repo/pull/123" - result = PullRequestNode.url(mock_pr) + field = self._get_field_by_name("url", PullRequestNode) + result = field.base_resolver.wrapped_func(None, mock_pr) assert result == "https://github.com/test-org/test-repo/pull/123" - - def test_url_without_url(self): - """Test url field when URL doesn't exist.""" - mock_pr = Mock() - mock_pr.url = None - - result = PullRequestNode.url(mock_pr) - assert result == "" diff --git a/backend/tests/apps/github/api/internal/nodes/release_test.py b/backend/tests/apps/github/api/internal/nodes/release_test.py index ef45238ffd..f897c8e40c 100644 --- a/backend/tests/apps/github/api/internal/nodes/release_test.py +++ b/backend/tests/apps/github/api/internal/nodes/release_test.py @@ -4,9 +4,10 @@ from apps.github.api.internal.nodes.release import ReleaseNode from apps.github.api.internal.nodes.user import UserNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestReleaseNode: +class TestReleaseNode(GraphQLNodeBaseTest): """Test cases for ReleaseNode class.""" def test_release_node_inheritance(self): @@ -34,15 +35,6 @@ def test_author_field(self): assert author_field is not None assert author_field.type.of_type is UserNode - def test_author_resolution(self): - """Test author field resolution.""" - mock_release = Mock() - mock_author = Mock() - mock_release.author = mock_author - - result = ReleaseNode.author(mock_release) - assert result == mock_author - def test_organization_name_with_organization(self): """Test organization_name field when organization exists.""" mock_release = Mock() @@ -52,7 +44,8 @@ def test_organization_name_with_organization(self): mock_repository.organization = mock_organization mock_release.repository = mock_repository - result = ReleaseNode.organization_name(mock_release) + field = self._get_field_by_name("organization_name", ReleaseNode) + result = field.base_resolver.wrapped_func(None, mock_release) assert result == "test-org" def test_organization_name_without_organization(self): @@ -62,7 +55,8 @@ def test_organization_name_without_organization(self): mock_repository.organization = None mock_release.repository = mock_repository - result = ReleaseNode.organization_name(mock_release) + field = self._get_field_by_name("organization_name", ReleaseNode) + result = field.base_resolver.wrapped_func(None, mock_release) assert result is None def test_organization_name_without_repository(self): @@ -70,7 +64,8 @@ def test_organization_name_without_repository(self): mock_release = Mock() mock_release.repository = None - result = ReleaseNode.organization_name(mock_release) + field = self._get_field_by_name("organization_name", ReleaseNode) + result = field.base_resolver.wrapped_func(None, mock_release) assert result is None def test_project_name_with_project(self): @@ -82,7 +77,8 @@ def test_project_name_with_project(self): mock_repository.project = mock_project mock_release.repository = mock_repository - result = ReleaseNode.project_name(mock_release) + field = self._get_field_by_name("project_name", ReleaseNode) + result = field.base_resolver.wrapped_func(None, mock_release) assert result == " Test Project" # OWASP prefix stripped def test_project_name_without_project(self): @@ -92,7 +88,8 @@ def test_project_name_without_project(self): mock_repository.project = None mock_release.repository = mock_repository - result = ReleaseNode.project_name(mock_release) + field = self._get_field_by_name("project_name", ReleaseNode) + result = field.base_resolver.wrapped_func(None, mock_release) assert result is None def test_project_name_without_repository(self): @@ -100,7 +97,8 @@ def test_project_name_without_repository(self): mock_release = Mock() mock_release.repository = None - result = ReleaseNode.project_name(mock_release) + field = self._get_field_by_name("project_name", ReleaseNode) + result = field.base_resolver.wrapped_func(None, mock_release) assert result is None def test_repository_name_with_repository(self): @@ -110,7 +108,8 @@ def test_repository_name_with_repository(self): mock_repository.name = "test-repo" mock_release.repository = mock_repository - result = ReleaseNode.repository_name(mock_release) + field = self._get_field_by_name("repository_name", ReleaseNode) + result = field.base_resolver.wrapped_func(None, mock_release) assert result == "test-repo" def test_repository_name_without_repository(self): @@ -118,7 +117,8 @@ def test_repository_name_without_repository(self): mock_release = Mock() mock_release.repository = None - result = ReleaseNode.repository_name(mock_release) + field = self._get_field_by_name("repository_name", ReleaseNode) + result = field.base_resolver.wrapped_func(None, mock_release) assert result is None def test_url_field(self): @@ -126,5 +126,6 @@ def test_url_field(self): mock_release = Mock() mock_release.url = "https://github.com/test-org/test-repo/releases/tag/v1.0.0" - result = ReleaseNode.url(mock_release) + field = self._get_field_by_name("url", ReleaseNode) + result = field.base_resolver.wrapped_func(None, mock_release) assert result == "https://github.com/test-org/test-repo/releases/tag/v1.0.0" diff --git a/backend/tests/apps/github/api/internal/nodes/repository_test.py b/backend/tests/apps/github/api/internal/nodes/repository_test.py index fd5ef8142c..757b91a0f2 100644 --- a/backend/tests/apps/github/api/internal/nodes/repository_test.py +++ b/backend/tests/apps/github/api/internal/nodes/repository_test.py @@ -8,9 +8,10 @@ from apps.github.api.internal.nodes.release import ReleaseNode from apps.github.api.internal.nodes.repository import RepositoryNode from apps.github.api.internal.nodes.repository_contributor import RepositoryContributorNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestRepositoryNode: +class TestRepositoryNode(GraphQLNodeBaseTest): def test_repository_node_inheritance(self): assert hasattr(RepositoryNode, "__strawberry_definition__") @@ -32,7 +33,6 @@ def test_meta_configuration(self): "name", "open_issues_count", "organization", - "owner_key", "project", "recent_milestones", "releases", @@ -46,58 +46,48 @@ def test_meta_configuration(self): } assert expected_field_names.issubset(field_names) - def _get_field_by_name(self, name): - return next( - (f for f in RepositoryNode.__strawberry_definition__.fields if f.name == name), None - ) - def test_resolve_issues(self): - field = self._get_field_by_name("issues") + field = self._get_field_by_name("issues", RepositoryNode) assert field is not None assert field.type.of_type is IssueNode def test_resolve_languages(self): - field = self._get_field_by_name("languages") + field = self._get_field_by_name("languages", RepositoryNode) assert field is not None assert field.type == list[str] def test_resolve_latest_release(self): - field = self._get_field_by_name("latest_release") + field = self._get_field_by_name("latest_release", RepositoryNode) assert field is not None - assert field.type is str + assert field.type.of_type is str def test_resolve_organization(self): - field = self._get_field_by_name("organization") + field = self._get_field_by_name("organization", RepositoryNode) assert field is not None assert field.type.of_type is OrganizationNode - def test_resolve_owner_key(self): - field = self._get_field_by_name("owner_key") - assert field is not None - assert field.type is str - def test_resolve_recent_milestones(self): - field = self._get_field_by_name("recent_milestones") + field = self._get_field_by_name("recent_milestones", RepositoryNode) assert field is not None assert field.type.of_type is MilestoneNode def test_resolve_releases(self): - field = self._get_field_by_name("releases") + field = self._get_field_by_name("releases", RepositoryNode) assert field is not None assert field.type.of_type is ReleaseNode def test_resolve_top_contributors(self): - field = self._get_field_by_name("top_contributors") + field = self._get_field_by_name("top_contributors", RepositoryNode) assert field is not None assert field.type.of_type is RepositoryContributorNode def test_resolve_topics(self): - field = self._get_field_by_name("topics") + field = self._get_field_by_name("topics", RepositoryNode) assert field is not None assert field.type == list[str] def test_resolve_url(self): - field = self._get_field_by_name("url") + field = self._get_field_by_name("url", RepositoryNode) assert field is not None assert field.type is str @@ -105,21 +95,20 @@ def test_issues_method(self): """Test issues method resolution.""" mock_repository = Mock() mock_issues = Mock() - mock_issues.select_related.return_value.order_by.return_value.__getitem__ = Mock( - return_value=[] - ) + mock_issues.order_by.return_value.__getitem__ = Mock(return_value=[]) mock_repository.issues = mock_issues - RepositoryNode.issues(mock_repository) - mock_issues.select_related.assert_called_with("author") - mock_issues.select_related.return_value.order_by.assert_called_with("-created_at") + field = self._get_field_by_name("issues", RepositoryNode) + field.base_resolver.wrapped_func(None, mock_repository) + mock_issues.order_by.assert_called_with("-created_at") def test_languages_method(self): """Test languages method resolution.""" mock_repository = Mock() mock_repository.languages = {"Python": 1000, "JavaScript": 500} - result = RepositoryNode.languages(mock_repository) + field = self._get_field_by_name("languages", RepositoryNode) + result = field.base_resolver.wrapped_func(None, mock_repository) assert result == ["Python", "JavaScript"] def test_latest_release_method(self): @@ -127,47 +116,31 @@ def test_latest_release_method(self): mock_repository = Mock() mock_repository.latest_release = "v1.0.0" - result = RepositoryNode.latest_release(mock_repository) + field = self._get_field_by_name("latest_release", RepositoryNode) + result = field.base_resolver.wrapped_func(None, mock_repository) assert result == "v1.0.0" - def test_organization_method(self): - """Test organization method resolution.""" - mock_repository = Mock() - mock_organization = Mock() - mock_repository.organization = mock_organization - - result = RepositoryNode.organization(mock_repository) - assert result == mock_organization - - def test_owner_key_method(self): - """Test owner_key method resolution.""" - mock_repository = Mock() - mock_repository.owner_key = "test-owner" - - result = RepositoryNode.owner_key(mock_repository) - assert result == "test-owner" - def test_project_method(self): """Test project method resolution.""" mock_repository = Mock() mock_project = Mock() mock_repository.project = mock_project - result = RepositoryNode.project(mock_repository) + field = self._get_field_by_name("project", RepositoryNode) + result = field.base_resolver.wrapped_func(None, mock_repository) assert result == mock_project def test_recent_milestones_method(self): """Test recent_milestones method resolution.""" mock_repository = Mock() mock_milestones = Mock() - mock_milestones.select_related.return_value.order_by.return_value.__getitem__ = Mock( - return_value=[] - ) + mock_milestones.order_by.return_value.__getitem__ = Mock(return_value=[]) mock_repository.recent_milestones = mock_milestones - RepositoryNode.recent_milestones(mock_repository, limit=3) - mock_milestones.select_related.assert_called_with("repository") - mock_milestones.select_related.return_value.order_by.assert_called_with("-created_at") + field = self._get_field_by_name("recent_milestones", RepositoryNode) + resolver = field.base_resolver.wrapped_func + resolver(None, mock_repository, limit=3) + mock_milestones.order_by.assert_called_with("-created_at") def test_releases_method(self): """Test releases method resolution.""" @@ -176,24 +149,43 @@ def test_releases_method(self): mock_releases.order_by.return_value.__getitem__ = Mock(return_value=[]) mock_repository.published_releases = mock_releases - RepositoryNode.releases(mock_repository) + field = self._get_field_by_name("releases", RepositoryNode) + resolver = field.base_resolver.wrapped_func + resolver(None, mock_repository) mock_releases.order_by.assert_called_with("-published_at") def test_top_contributors_method(self): """Test top_contributors method resolution.""" mock_repository = Mock() - mock_contributors = [Mock(), Mock()] - mock_repository.idx_top_contributors = mock_contributors - - result = RepositoryNode.top_contributors(mock_repository) - assert result == mock_contributors + mock_repository.idx_top_contributors = [ + { + "avatar_url": "url1", + "contributions_count": 100, + "id": "user1", + "login": "user1", + "name": "User 1", + }, + { + "avatar_url": "url2", + "contributions_count": 50, + "id": "user2", + "login": "user2", + "name": "User 2", + }, + ] + + field = self._get_field_by_name("top_contributors", RepositoryNode) + result = field.base_resolver.wrapped_func(None, mock_repository) + assert len(result) == 2 + assert all(isinstance(c, RepositoryContributorNode) for c in result) def test_topics_method(self): """Test topics method resolution.""" mock_repository = Mock() mock_repository.topics = ["security", "python", "django"] - result = RepositoryNode.topics(mock_repository) + field = self._get_field_by_name("topics", RepositoryNode) + result = field.base_resolver.wrapped_func(None, mock_repository) assert result == ["security", "python", "django"] def test_url_method(self): @@ -201,7 +193,8 @@ def test_url_method(self): mock_repository = Mock() mock_repository.url = "https://github.com/test-org/test-repo" - result = RepositoryNode.url(mock_repository) + field = self._get_field_by_name("url", RepositoryNode) + result = field.base_resolver.wrapped_func(None, mock_repository) assert result == "https://github.com/test-org/test-repo" def test_is_archived_field_exists(self): @@ -213,6 +206,6 @@ def test_is_archived_field_exists(self): def test_resolve_is_archived(self): """Test is_archived field type.""" - field = self._get_field_by_name("is_archived") + field = self._get_field_by_name("is_archived", RepositoryNode) assert field is not None assert field.type is bool diff --git a/backend/tests/apps/github/api/internal/nodes/user_test.py b/backend/tests/apps/github/api/internal/nodes/user_test.py index ee4945db26..b9356ec5a1 100644 --- a/backend/tests/apps/github/api/internal/nodes/user_test.py +++ b/backend/tests/apps/github/api/internal/nodes/user_test.py @@ -1,13 +1,15 @@ """Test cases for UserNode.""" import math +from datetime import UTC, datetime from unittest.mock import Mock from apps.github.api.internal.nodes.user import UserNode from apps.nest.api.internal.nodes.badge import BadgeNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestUserNode: +class TestUserNode(GraphQLNodeBaseTest): """Test cases for UserNode class.""" def test_user_node_inheritance(self): @@ -52,7 +54,8 @@ def test_created_at_field(self): mock_user = Mock() mock_user.idx_created_at = 1234567890.0 - result = UserNode.created_at(mock_user) + field = self._get_field_by_name("created_at", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert math.isclose(result, 1234567890.0) def test_issues_count_field(self): @@ -60,7 +63,8 @@ def test_issues_count_field(self): mock_user = Mock() mock_user.idx_issues_count = 42 - result = UserNode.issues_count(mock_user) + field = self._get_field_by_name("issues_count", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result == 42 def test_releases_count_field(self): @@ -68,7 +72,8 @@ def test_releases_count_field(self): mock_user = Mock() mock_user.idx_releases_count = 15 - result = UserNode.releases_count(mock_user) + field = self._get_field_by_name("releases_count", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result == 15 def test_updated_at_field(self): @@ -76,7 +81,8 @@ def test_updated_at_field(self): mock_user = Mock() mock_user.idx_updated_at = 1234567890.0 - result = UserNode.updated_at(mock_user) + field = self._get_field_by_name("updated_at", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert math.isclose(result, 1234567890.0) def test_url_field(self): @@ -84,7 +90,8 @@ def test_url_field(self): mock_user = Mock() mock_user.url = "https://github.com/testuser" - result = UserNode.url(mock_user) + field = self._get_field_by_name("url", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result == "https://github.com/testuser" def test_badge_count_field(self): @@ -94,7 +101,8 @@ def test_badge_count_field(self): mock_badges_queryset.filter.return_value.count.return_value = 3 mock_user.user_badges = mock_badges_queryset - result = UserNode.badge_count(mock_user) + field = self._get_field_by_name("badge_count", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result == 3 mock_badges_queryset.filter.assert_called_once_with(is_active=True) mock_badges_queryset.filter.return_value.count.assert_called_once() @@ -108,7 +116,8 @@ def test_badges_field_empty(self): mock_select_related.order_by.return_value = [] mock_user.user_badges = mock_badges_queryset - result = UserNode.badges(mock_user) + field = self._get_field_by_name("badges", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result == [] mock_badges_queryset.filter.assert_called_once_with(is_active=True) mock_filter.select_related.assert_called_once_with("badge") @@ -127,7 +136,8 @@ def test_badges_field_single_badge(self): mock_select_related.order_by.return_value = [mock_user_badge] mock_user.user_badges = mock_badges_queryset - result = UserNode.badges(mock_user) + field = self._get_field_by_name("badges", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result == [mock_badge] mock_badges_queryset.filter.assert_called_once_with(is_active=True) mock_filter.select_related.assert_called_once_with("badge") @@ -179,7 +189,8 @@ def test_badges_field_sorted_by_weight_and_name(self): mock_user = Mock() mock_user.user_badges = mock_badges_queryset - result = UserNode.badges(mock_user) + field = self._get_field_by_name("badges", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) # Verify the badges are returned in the correct order expected_badges = [ @@ -192,20 +203,20 @@ def test_badges_field_sorted_by_weight_and_name(self): # Verify the queryset was called with correct ordering mock_badges_queryset.filter.assert_called_once_with(is_active=True) - mock_filter.select_related.assert_called_once_with("badge") - mock_select_related.order_by.assert_called_once_with("badge__weight", "badge__name") + mock_filter.select_related.return_value.order_by.assert_called_once_with( + "badge__weight", "badge__name" + ) def test_first_owasp_contribution_at_with_profile(self): """Test first_owasp_contribution_at returns timestamp when profile exists.""" - from datetime import UTC, datetime - mock_profile = Mock() mock_profile.first_contribution_at = datetime(2025, 1, 15, tzinfo=UTC) mock_user = Mock() mock_user.owasp_profile = mock_profile - result = UserNode.first_owasp_contribution_at(mock_user) + field = self._get_field_by_name("first_owasp_contribution_at", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result == mock_profile.first_contribution_at.timestamp() @@ -213,7 +224,8 @@ def test_first_owasp_contribution_at_without_profile(self): """Test first_owasp_contribution_at returns None when no profile.""" mock_user = Mock(spec=[]) - result = UserNode.first_owasp_contribution_at(mock_user) + field = self._get_field_by_name("first_owasp_contribution_at", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result is None @@ -225,7 +237,8 @@ def test_is_owasp_board_member_true(self): mock_user = Mock() mock_user.owasp_profile = mock_profile - result = UserNode.is_owasp_board_member(mock_user) + field = self._get_field_by_name("is_owasp_board_member", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result is True @@ -233,7 +246,8 @@ def test_is_owasp_board_member_without_profile(self): """Test is_owasp_board_member returns False when no profile.""" mock_user = Mock(spec=[]) - result = UserNode.is_owasp_board_member(mock_user) + field = self._get_field_by_name("is_owasp_board_member", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result is False @@ -245,7 +259,8 @@ def test_is_former_owasp_staff_true(self): mock_user = Mock() mock_user.owasp_profile = mock_profile - result = UserNode.is_former_owasp_staff(mock_user) + field = self._get_field_by_name("is_former_owasp_staff", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result is True @@ -253,7 +268,8 @@ def test_is_former_owasp_staff_without_profile(self): """Test is_former_owasp_staff returns False when no profile.""" mock_user = Mock(spec=[]) - result = UserNode.is_former_owasp_staff(mock_user) + field = self._get_field_by_name("is_former_owasp_staff", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result is False @@ -265,7 +281,8 @@ def test_is_gsoc_mentor_true(self): mock_user = Mock() mock_user.owasp_profile = mock_profile - result = UserNode.is_gsoc_mentor(mock_user) + field = self._get_field_by_name("is_gsoc_mentor", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result is True @@ -273,7 +290,8 @@ def test_is_gsoc_mentor_without_profile(self): """Test is_gsoc_mentor returns False when no profile.""" mock_user = Mock(spec=[]) - result = UserNode.is_gsoc_mentor(mock_user) + field = self._get_field_by_name("is_gsoc_mentor", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result is False @@ -285,7 +303,8 @@ def test_linkedin_page_id_with_profile_and_value(self): mock_user = Mock() mock_user.owasp_profile = mock_profile - result = UserNode.linkedin_page_id(mock_user) + field = self._get_field_by_name("linkedin_page_id", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result == "john-doe-123" @@ -297,7 +316,8 @@ def test_linkedin_page_id_with_profile_empty_value(self): mock_user = Mock() mock_user.owasp_profile = mock_profile - result = UserNode.linkedin_page_id(mock_user) + field = self._get_field_by_name("linkedin_page_id", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result == "" @@ -305,6 +325,7 @@ def test_linkedin_page_id_without_profile(self): """Test linkedin_page_id returns empty string when no profile.""" mock_user = Mock(spec=[]) - result = UserNode.linkedin_page_id(mock_user) + field = self._get_field_by_name("linkedin_page_id", UserNode) + result = field.base_resolver.wrapped_func(None, mock_user) assert result == "" diff --git a/backend/tests/apps/github/api/internal/queries/issue_test.py b/backend/tests/apps/github/api/internal/queries/issue_test.py index ac29143a96..2932f68b7a 100644 --- a/backend/tests/apps/github/api/internal/queries/issue_test.py +++ b/backend/tests/apps/github/api/internal/queries/issue_test.py @@ -19,172 +19,161 @@ def mock_issue(self): issue.author_id = 42 return issue - @patch("apps.github.models.issue.Issue.objects.select_related") - def test_recent_issues_basic(self, mock_select_related, mock_issue): + @patch("apps.github.models.issue.Issue.objects") + def test_recent_issues_basic(self, mock_objects, mock_issue): """Test fetching recent issues with default parameters.""" mock_queryset = MagicMock() - mock_queryset.order_by.return_value = [mock_issue] - mock_select_related.return_value = mock_queryset + mock_queryset.order_by.return_value = mock_queryset + mock_queryset.filter.return_value = mock_queryset + mock_queryset.__getitem__.return_value = [mock_issue] + mock_objects.order_by.return_value = mock_queryset result = IssueQuery().recent_issues() assert result == [mock_issue] - mock_select_related.assert_called_once_with( - "author", "repository", "repository__organization" - ) - mock_queryset.order_by.assert_called_once_with("-created_at") + mock_objects.order_by.assert_called_once_with("-created_at") - @patch("apps.github.models.issue.Issue.objects.select_related") - def test_recent_issues_with_login(self, mock_select_related, mock_issue): + @patch("apps.github.models.issue.Issue.objects") + def test_recent_issues_with_login(self, mock_objects, mock_issue): """Test filtering issues by login.""" mock_queryset = MagicMock() - mock_queryset.order_by.return_value.filter.return_value = [mock_issue] - mock_select_related.return_value = mock_queryset + mock_queryset.order_by.return_value = mock_queryset + mock_queryset.filter.return_value = mock_queryset + mock_queryset.__getitem__.return_value = [mock_issue] + mock_objects.order_by.return_value = mock_queryset result = IssueQuery().recent_issues(login="alice") - mock_select_related.assert_called_once() - mock_queryset.order_by.assert_called_once() - mock_queryset.order_by.return_value.filter.assert_called_once_with(author__login="alice") + mock_queryset.filter.assert_called_once_with(author__login="alice") assert result == [mock_issue] - @patch("apps.github.models.issue.Issue.objects.select_related") - def test_recent_issues_with_organization(self, mock_select_related, mock_issue): + @patch("apps.github.models.issue.Issue.objects") + def test_recent_issues_with_organization(self, mock_objects, mock_issue): """Test filtering issues by organization.""" mock_queryset = MagicMock() - mock_queryset.order_by.return_value.filter.return_value = [mock_issue] - mock_select_related.return_value = mock_queryset + mock_queryset.order_by.return_value = mock_queryset + mock_queryset.filter.return_value = mock_queryset + mock_queryset.__getitem__.return_value = [mock_issue] + mock_objects.order_by.return_value = mock_queryset result = IssueQuery().recent_issues(organization="orgX") - mock_queryset.order_by.assert_called_once() - mock_queryset.order_by.return_value.filter.assert_called_once_with( - repository__organization__login="orgX" - ) + mock_queryset.filter.assert_called_once_with(repository__organization__login="orgX") assert result == [mock_issue] - @patch("apps.github.models.issue.Issue.objects.select_related") - def test_recent_issues_limit(self, mock_select_related, mock_issue): + @patch("apps.github.models.issue.Issue.objects") + def test_recent_issues_limit(self, mock_objects, mock_issue): """Test limiting the number of issues returned.""" mock_queryset = MagicMock() - mock_queryset.order_by.return_value.__getitem__.return_value = [mock_issue] - mock_select_related.return_value = mock_queryset + mock_queryset.order_by.return_value = mock_queryset + mock_queryset.filter.return_value = mock_queryset + mock_queryset.__getitem__.return_value = [mock_issue] + mock_objects.order_by.return_value = mock_queryset result = IssueQuery().recent_issues(limit=1) - mock_select_related.assert_called_once() - mock_queryset.order_by.assert_called_once_with("-created_at") + mock_objects.order_by.assert_called_once_with("-created_at") assert result == [mock_issue] - @patch("apps.github.api.internal.queries.issue.Subquery") - @patch("apps.github.models.issue.Issue.objects.select_related") - def test_recent_issues_distinct(self, mock_select_related, mock_subquery, mock_issue): - """Test distinct filtering with Subquery for issues.""" + @patch("apps.github.models.issue.Issue.objects") + def test_recent_issues_distinct(self, mock_objects, mock_issue): + """Test distinct filtering with Window/Rank for issues.""" mock_queryset = MagicMock() mock_queryset.order_by.return_value = mock_queryset mock_queryset.filter.return_value = mock_queryset + mock_queryset.annotate.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_issue] - mock_select_related.return_value = mock_queryset - - mock_subquery_instance = MagicMock() - mock_subquery.return_value = mock_subquery_instance + mock_objects.order_by.return_value = mock_queryset result = IssueQuery().recent_issues(distinct=True) assert result == [mock_issue] - mock_subquery.assert_called_once() - mock_queryset.filter.assert_called() - - @patch("apps.github.models.issue.Issue.objects.select_related") - def test_recent_issues_combined_filters(self, mock_select_related, mock_issue): + mock_queryset.annotate.assert_called_once() + # One filter for empty filters dict, one for rank=1 + assert mock_queryset.filter.call_count == 2 + assert ( + mock_queryset.order_by.call_count == 1 + ) # Only initially, distinct adds order_by directly + + @patch("apps.github.models.issue.Issue.objects") + def test_recent_issues_combined_filters(self, mock_objects, mock_issue): mock_queryset = MagicMock() - mock_queryset.order_by.return_value.filter.return_value.filter.return_value = [mock_issue] - mock_select_related.return_value = mock_queryset + mock_queryset.order_by.return_value = mock_queryset + mock_queryset.filter.return_value = mock_queryset + mock_queryset.__getitem__.return_value = [mock_issue] + mock_objects.order_by.return_value = mock_queryset result = IssueQuery().recent_issues(login="alice", organization="owasp") - mock_select_related.assert_called_once() - mock_queryset.order_by.assert_called_once() + # Both filters are applied in one call using **filters + mock_queryset.filter.assert_called_once_with( + author__login="alice", repository__organization__login="owasp" + ) assert result == [mock_issue] - @patch("apps.github.api.internal.queries.issue.OuterRef") - @patch("apps.github.api.internal.queries.issue.Subquery") - @patch("apps.github.models.issue.Issue.objects.select_related") - def test_recent_issues_distinct_with_organization( - self, mock_select_related, mock_subquery, mock_outer_ref, mock_issue - ): + @patch("apps.github.models.issue.Issue.objects") + def test_recent_issues_distinct_with_organization(self, mock_objects, mock_issue): """Test distinct filtering with organization filter.""" mock_queryset = MagicMock() mock_queryset.order_by.return_value = mock_queryset mock_queryset.filter.return_value = mock_queryset + mock_queryset.annotate.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_issue] - mock_select_related.return_value = mock_queryset - - mock_subquery_instance = Mock() - mock_subquery.return_value = mock_subquery_instance - mock_outer_ref_instance = Mock() - mock_outer_ref.return_value = mock_outer_ref_instance + mock_objects.order_by.return_value = mock_queryset result = IssueQuery().recent_issues(distinct=True, organization="owasp") assert result == [mock_issue] - mock_subquery.assert_called_once() - mock_outer_ref.assert_called_once_with("author_id") - - @patch("apps.github.api.internal.queries.issue.OuterRef") - @patch("apps.github.api.internal.queries.issue.Subquery") - @patch("apps.github.models.issue.Issue.objects.select_related") - def test_recent_issues_distinct_with_all_filters( - self, mock_select_related, mock_subquery, mock_outer_ref, mock_issue - ): + mock_queryset.annotate.assert_called_once() + # One filter for organization, one for rank=1 + assert mock_queryset.filter.call_count == 2 + + @patch("apps.github.models.issue.Issue.objects") + def test_recent_issues_distinct_with_all_filters(self, mock_objects, mock_issue): """Test distinct filtering with all filters.""" mock_queryset = MagicMock() mock_queryset.order_by.return_value = mock_queryset mock_queryset.filter.return_value = mock_queryset + mock_queryset.annotate.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_issue] - mock_select_related.return_value = mock_queryset - - mock_subquery_instance = Mock() - mock_subquery.return_value = mock_subquery_instance - mock_outer_ref_instance = Mock() - mock_outer_ref.return_value = mock_outer_ref_instance + mock_objects.order_by.return_value = mock_queryset result = IssueQuery().recent_issues( distinct=True, login="alice", organization="owasp", limit=3 ) assert result == [mock_issue] - mock_subquery.assert_called_once() - mock_outer_ref.assert_called_once_with("author_id") - # Verify both login and organization filters were applied - assert mock_queryset.filter.call_count >= 3 # login, organization, distinct + mock_queryset.annotate.assert_called_once() + # One filter for login+organization combined, one for rank=1 + assert mock_queryset.filter.call_count == 2 + mock_queryset.__getitem__.assert_called_with(slice(None, 3)) - @patch("apps.github.models.issue.Issue.objects.select_related") - def test_recent_issues_organization_only(self, mock_select_related, mock_issue): + @patch("apps.github.models.issue.Issue.objects") + def test_recent_issues_organization_only(self, mock_objects, mock_issue): """Test filtering issues by organization only.""" mock_queryset = MagicMock() mock_queryset.order_by.return_value = mock_queryset mock_queryset.filter.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_issue] - mock_select_related.return_value = mock_queryset + mock_objects.order_by.return_value = mock_queryset result = IssueQuery().recent_issues(organization="owasp") assert result == [mock_issue] - assert len(mock_queryset.filter.call_args_list) >= 1 + mock_queryset.filter.assert_called_once_with(repository__organization__login="owasp") - @patch("apps.github.models.issue.Issue.objects.select_related") - def test_recent_issues_multiple_filters(self, mock_select_related, mock_issue): + @patch("apps.github.models.issue.Issue.objects") + def test_recent_issues_multiple_filters(self, mock_objects, mock_issue): """Test issues with multiple filters applied.""" mock_queryset = MagicMock() mock_queryset.order_by.return_value = mock_queryset mock_queryset.filter.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_issue] - mock_select_related.return_value = mock_queryset + mock_objects.order_by.return_value = mock_queryset result = IssueQuery().recent_issues(organization="owasp", limit=10) assert result == [mock_issue] # Verify organization filter was applied - assert len(mock_queryset.filter.call_args_list) >= 1 + mock_queryset.filter.assert_called_once_with(repository__organization__login="owasp") mock_queryset.__getitem__.assert_called_with(slice(None, 10)) diff --git a/backend/tests/apps/github/api/internal/queries/milestone_test.py b/backend/tests/apps/github/api/internal/queries/milestone_test.py index d1c4689b45..fb4967c6ac 100644 --- a/backend/tests/apps/github/api/internal/queries/milestone_test.py +++ b/backend/tests/apps/github/api/internal/queries/milestone_test.py @@ -3,9 +3,8 @@ from unittest.mock import MagicMock, Mock, patch import pytest -from django.core.exceptions import ValidationError -from apps.github.api.internal.queries.milestone import MilestoneQuery +from apps.github.api.internal.queries.milestone import MilestoneQuery, MilestoneStateEnum from apps.github.models.milestone import Milestone @@ -22,7 +21,6 @@ def mock_milestone(self): def get_queryset(self): """Return a mocked queryset.""" queryset = MagicMock() - queryset.select_related.return_value.prefetch_related.return_value = queryset queryset.filter.return_value = queryset queryset.order_by.return_value = queryset queryset.__getitem__.return_value = [Mock()] @@ -31,9 +29,9 @@ def get_queryset(self): @pytest.mark.parametrize( ("state", "manager"), [ - ("open", "open_milestones"), - ("closed", "closed_milestones"), - ("all", "objects"), + (MilestoneStateEnum.OPEN, "open_milestones"), + (MilestoneStateEnum.CLOSED, "closed_milestones"), + (None, "objects"), ], ) def test_recent_milestones_by_state(self, get_queryset, state, manager): @@ -41,9 +39,6 @@ def test_recent_milestones_by_state(self, get_queryset, state, manager): with patch.object(Milestone, manager, new_callable=Mock) as mock_manager: mock_manager.all.return_value = get_queryset - get_queryset.select_related.return_value = get_queryset - get_queryset.prefetch_related.return_value = get_queryset - result = MilestoneQuery().recent_milestones( distinct=False, limit=5, @@ -53,8 +48,6 @@ def test_recent_milestones_by_state(self, get_queryset, state, manager): ) assert isinstance(result, list) - assert get_queryset.select_related.called - assert get_queryset.prefetch_related.called def test_recent_milestones_with_filters(self, get_queryset): """Test recent milestones with login and organization filters.""" @@ -66,7 +59,7 @@ def test_recent_milestones_with_filters(self, get_queryset): limit=3, login="user", organization="github", - state="open", + state=MilestoneStateEnum.OPEN, ) get_queryset.filter.assert_any_call(author__login="user") @@ -78,7 +71,6 @@ def test_recent_milestones_distinct(self): with patch.object(Milestone, "open_milestones", new_callable=Mock) as mock_manager: base_queryset = MagicMock() filtered_queryset = MagicMock() - base_queryset.select_related.return_value.prefetch_related.return_value = base_queryset base_queryset.filter.return_value = filtered_queryset filtered_queryset.filter.return_value = filtered_queryset filtered_queryset.order_by.return_value = filtered_queryset @@ -90,27 +82,23 @@ def test_recent_milestones_distinct(self): limit=1, login=None, organization=None, - state="open", + state=MilestoneStateEnum.OPEN, ) assert isinstance(result, list) assert filtered_queryset.__getitem__.called - def test_recent_milestones_invalid_state(self): - """Test ValidationError for invalid state parameter.""" - with pytest.raises(ValidationError) as exc_info: - MilestoneQuery().recent_milestones(state="invalid") - - assert "Invalid state: invalid" in str(exc_info.value) - assert "Valid states are 'open', 'closed', or 'all'" in str(exc_info.value) - def test_recent_milestones_with_all_parameters(self, get_queryset): """Test recent milestones with all parameters provided.""" with patch.object(Milestone, "closed_milestones", new_callable=Mock) as mock_manager: mock_manager.all.return_value = get_queryset result = MilestoneQuery().recent_milestones( - distinct=False, limit=10, login="testuser", organization="owasp", state="closed" + distinct=False, + limit=10, + login="testuser", + organization="owasp", + state=MilestoneStateEnum.CLOSED, ) assert isinstance(result, list) diff --git a/backend/tests/apps/github/api/internal/queries/pull_request_test.py b/backend/tests/apps/github/api/internal/queries/pull_request_test.py index 574ea82c8c..911b8e41bc 100644 --- a/backend/tests/apps/github/api/internal/queries/pull_request_test.py +++ b/backend/tests/apps/github/api/internal/queries/pull_request_test.py @@ -23,32 +23,29 @@ def mock_pull_request(self): def mock_queryset(self): """Mock queryset with all necessary methods.""" queryset = MagicMock() - queryset.select_related.return_value = queryset queryset.exclude.return_value = queryset queryset.order_by.return_value = queryset queryset.filter.return_value = queryset + queryset.annotate.return_value = queryset queryset.__getitem__.return_value = [] return queryset @patch("apps.github.models.pull_request.PullRequest.objects") def test_recent_pull_requests_basic(self, mock_objects, mock_queryset, mock_pull_request): """Test fetching recent pull requests with default parameters.""" - mock_objects.select_related.return_value = mock_queryset + mock_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] result = PullRequestQuery().recent_pull_requests() assert result == [mock_pull_request] - mock_objects.select_related.assert_called_once_with( - "author", "repository", "repository__organization" - ) - mock_queryset.exclude.assert_called_once_with(author__is_bot=True) + mock_objects.exclude.assert_called_once_with(author__is_bot=True) mock_queryset.order_by.assert_called_once_with("-created_at") @patch("apps.github.models.pull_request.PullRequest.objects") def test_recent_pull_requests_with_login(self, mock_objects, mock_queryset, mock_pull_request): """Test filtering pull requests by login.""" - mock_objects.select_related.return_value = mock_queryset + mock_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] result = PullRequestQuery().recent_pull_requests(login="alice") @@ -61,7 +58,7 @@ def test_recent_pull_requests_with_organization( self, mock_objects, mock_queryset, mock_pull_request ): """Test filtering pull requests by organization.""" - mock_objects.select_related.return_value = mock_queryset + mock_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] result = PullRequestQuery().recent_pull_requests(organization="owasp") @@ -79,7 +76,7 @@ def test_recent_pull_requests_with_project( mock_project.repositories.values_list.return_value = [1, 2, 3] mock_project_objects.filter.return_value.first.return_value = mock_project - mock_pr_objects.select_related.return_value = mock_queryset + mock_pr_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] result = PullRequestQuery().recent_pull_requests(project="test-project") @@ -93,7 +90,7 @@ def test_recent_pull_requests_with_repository( self, mock_objects, mock_queryset, mock_pull_request ): """Test filtering pull requests by repository.""" - mock_objects.select_related.return_value = mock_queryset + mock_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] result = PullRequestQuery().recent_pull_requests(repository="test-repo") @@ -101,28 +98,23 @@ def test_recent_pull_requests_with_repository( assert result == [mock_pull_request] mock_queryset.filter.assert_called_with(repository__key__iexact="test-repo") - @patch("apps.github.api.internal.queries.pull_request.Subquery") @patch("apps.github.models.pull_request.PullRequest.objects") - def test_recent_pull_requests_distinct( - self, mock_objects, mock_subquery, mock_queryset, mock_pull_request - ): - """Test distinct filtering with Subquery for pull requests.""" - mock_objects.select_related.return_value = mock_queryset + def test_recent_pull_requests_distinct(self, mock_objects, mock_queryset, mock_pull_request): + """Test distinct filtering with Window/Rank for pull requests.""" + mock_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] - mock_subquery_instance = Mock() - mock_subquery.return_value = mock_subquery_instance - result = PullRequestQuery().recent_pull_requests(distinct=True) assert result == [mock_pull_request] + mock_queryset.annotate.assert_called_once() mock_queryset.filter.assert_called() - mock_subquery.assert_called_once() + assert mock_queryset.order_by.call_count == 2 # Once initially, once after filter @patch("apps.github.models.pull_request.PullRequest.objects") def test_recent_pull_requests_with_limit(self, mock_objects, mock_queryset, mock_pull_request): """Test limiting the number of pull requests returned.""" - mock_objects.select_related.return_value = mock_queryset + mock_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] result = PullRequestQuery().recent_pull_requests(limit=3) @@ -135,7 +127,7 @@ def test_recent_pull_requests_multiple_filters( self, mock_objects, mock_queryset, mock_pull_request ): """Test pull requests with multiple filters applied.""" - mock_objects.select_related.return_value = mock_queryset + mock_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] result = PullRequestQuery().recent_pull_requests( @@ -143,39 +135,33 @@ def test_recent_pull_requests_multiple_filters( ) assert result == [mock_pull_request] - assert mock_queryset.filter.call_count >= 3 + # login+organization applied together, repository applied separately = 2 calls + assert mock_queryset.filter.call_count == 2 mock_queryset.__getitem__.assert_called_with(slice(None, 2)) - @patch("apps.github.api.internal.queries.pull_request.OuterRef") - @patch("apps.github.api.internal.queries.pull_request.Subquery") @patch("apps.github.models.pull_request.PullRequest.objects") def test_recent_pull_requests_distinct_with_filters( - self, mock_objects, mock_subquery, mock_outer_ref, mock_queryset, mock_pull_request + self, mock_objects, mock_queryset, mock_pull_request ): """Test distinct filtering with additional filters.""" - mock_objects.select_related.return_value = mock_queryset + mock_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] - mock_subquery_instance = Mock() - mock_subquery.return_value = mock_subquery_instance - mock_outer_ref_instance = Mock() - mock_outer_ref.return_value = mock_outer_ref_instance - result = PullRequestQuery().recent_pull_requests( distinct=True, login="alice", organization="owasp" ) assert result == [mock_pull_request] - mock_queryset.filter.assert_called() - mock_subquery.assert_called_once() - mock_outer_ref.assert_called_once_with("author_id") + mock_queryset.annotate.assert_called_once() + # One filter for login+organization combined, one for rank=1 + assert mock_queryset.filter.call_count == 2 @patch("apps.github.models.pull_request.PullRequest.objects") def test_recent_pull_requests_with_multiple_filters_no_project( self, mock_objects, mock_queryset, mock_pull_request ): """Test pull requests with multiple filters (no project to avoid DB issues).""" - mock_objects.select_related.return_value = mock_queryset + mock_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] result = PullRequestQuery().recent_pull_requests( @@ -183,30 +169,22 @@ def test_recent_pull_requests_with_multiple_filters_no_project( ) assert result == [mock_pull_request] - # Verify filters were applied - assert mock_queryset.filter.call_count >= 3 # login, organization, repository + # Verify filters were applied: login+organization together, repository separately + assert mock_queryset.filter.call_count == 2 - @patch("apps.github.api.internal.queries.pull_request.OuterRef") - @patch("apps.github.api.internal.queries.pull_request.Subquery") @patch("apps.github.models.pull_request.PullRequest.objects") def test_recent_pull_requests_distinct_comprehensive( - self, mock_objects, mock_subquery, mock_outer_ref, mock_queryset, mock_pull_request + self, mock_objects, mock_queryset, mock_pull_request ): """Test distinct filtering with multiple filters.""" - mock_objects.select_related.return_value = mock_queryset + mock_objects.exclude.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_pull_request] - mock_subquery_instance = Mock() - mock_subquery.return_value = mock_subquery_instance - mock_outer_ref_instance = Mock() - mock_outer_ref.return_value = mock_outer_ref_instance - result = PullRequestQuery().recent_pull_requests( distinct=True, login="alice", organization="owasp", repository="test-repo" ) assert result == [mock_pull_request] - mock_subquery.assert_called_once() - mock_outer_ref.assert_called_once_with("author_id") - # Verify multiple filters were applied - assert mock_queryset.filter.call_count >= 4 # login, org, repo, distinct + mock_queryset.annotate.assert_called_once() + # One filter for login+organization, one for repository, one for rank=1 + assert mock_queryset.filter.call_count == 3 diff --git a/backend/tests/apps/github/api/internal/queries/release_test.py b/backend/tests/apps/github/api/internal/queries/release_test.py index 388dc9b00c..fa93cdb5ed 100644 --- a/backend/tests/apps/github/api/internal/queries/release_test.py +++ b/backend/tests/apps/github/api/internal/queries/release_test.py @@ -25,7 +25,7 @@ def mock_queryset(self): queryset = MagicMock() queryset.filter.return_value = queryset queryset.order_by.return_value = queryset - queryset.select_related.return_value = queryset + queryset.annotate.return_value = queryset queryset.__getitem__.return_value = [] return queryset @@ -54,9 +54,6 @@ def test_recent_releases_with_login(self, mock_objects, mock_queryset, mock_rele result = ReleaseQuery().recent_releases(login="alice") assert result == [mock_release] - mock_queryset.select_related.assert_called_with( - "author", "repository", "repository__organization" - ) mock_queryset.filter.assert_called_with(author__login="alice") @patch("apps.github.models.release.Release.objects") @@ -70,23 +67,18 @@ def test_recent_releases_with_organization(self, mock_objects, mock_queryset, mo assert result == [mock_release] mock_queryset.filter.assert_called_with(repository__organization__login="owasp") - @patch("apps.github.api.internal.queries.release.Subquery") @patch("apps.github.models.release.Release.objects") - def test_recent_releases_distinct( - self, mock_objects, mock_subquery, mock_queryset, mock_release - ): - """Test distinct filtering with Subquery for releases.""" + def test_recent_releases_distinct(self, mock_objects, mock_queryset, mock_release): + """Test distinct filtering with Window/Rank for releases.""" mock_objects.filter.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_release] - mock_subquery_instance = Mock() - mock_subquery.return_value = mock_subquery_instance - result = ReleaseQuery().recent_releases(distinct=True) assert result == [mock_release] + mock_queryset.annotate.assert_called_once() mock_queryset.filter.assert_called() - mock_subquery.assert_called_once() + assert mock_queryset.order_by.call_count == 2 # Once initially, once after filter @patch("apps.github.models.release.Release.objects") def test_recent_releases_with_limit(self, mock_objects, mock_queryset, mock_release): @@ -103,118 +95,73 @@ def test_recent_releases_with_limit(self, mock_objects, mock_queryset, mock_rele def test_recent_releases_multiple_filters(self, mock_objects, mock_queryset, mock_release): """Test releases with multiple filters applied.""" mock_objects.filter.return_value = mock_queryset - mock_queryset.select_related.return_value = mock_queryset - mock_queryset.filter.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_release] result = ReleaseQuery().recent_releases(login="alice", organization="owasp", limit=2) assert result == [mock_release] - mock_queryset.select_related.assert_called_once_with( - "author", "repository", "repository__organization" - ) # Verify both filters were applied filter_calls = mock_queryset.filter.call_args_list assert len(filter_calls) >= 2 mock_queryset.__getitem__.assert_called_with(slice(None, 2)) - @patch("apps.github.api.internal.queries.release.OuterRef") - @patch("apps.github.api.internal.queries.release.Subquery") @patch("apps.github.models.release.Release.objects") - def test_recent_releases_distinct_with_login( - self, mock_objects, mock_subquery, mock_outer_ref, mock_queryset, mock_release - ): + def test_recent_releases_distinct_with_login(self, mock_objects, mock_queryset, mock_release): """Test distinct filtering with login filter.""" mock_objects.filter.return_value = mock_queryset - mock_queryset.select_related.return_value = mock_queryset - mock_queryset.filter.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_release] - mock_subquery_instance = Mock() - mock_subquery.return_value = mock_subquery_instance - mock_outer_ref_instance = Mock() - mock_outer_ref.return_value = mock_outer_ref_instance - result = ReleaseQuery().recent_releases(distinct=True, login="alice") assert result == [mock_release] - mock_subquery.assert_called_once() - mock_outer_ref.assert_called_once_with("author_id") - mock_queryset.select_related.assert_called_once_with( - "author", "repository", "repository__organization" - ) + mock_queryset.annotate.assert_called_once() + # One filter for login, one for rank=1 + # (initial filter is on Release.objects, not mock_queryset) + assert mock_queryset.filter.call_count == 2 - @patch("apps.github.api.internal.queries.release.OuterRef") - @patch("apps.github.api.internal.queries.release.Subquery") @patch("apps.github.models.release.Release.objects") def test_recent_releases_distinct_with_organization( - self, mock_objects, mock_subquery, mock_outer_ref, mock_queryset, mock_release + self, mock_objects, mock_queryset, mock_release ): """Test distinct filtering with organization filter.""" mock_objects.filter.return_value = mock_queryset - mock_queryset.filter.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_release] - mock_subquery_instance = Mock() - mock_subquery.return_value = mock_subquery_instance - mock_outer_ref_instance = Mock() - mock_outer_ref.return_value = mock_outer_ref_instance - result = ReleaseQuery().recent_releases(distinct=True, organization="owasp") assert result == [mock_release] - mock_subquery.assert_called_once() - mock_outer_ref.assert_called_once_with("author_id") - # Verify organization filter was applied - filter_calls = mock_queryset.filter.call_args_list - organization_filter_found = any( - "repository__organization__login" in str(call) for call in filter_calls - ) - assert organization_filter_found, "Organization filter should be applied" + mock_queryset.annotate.assert_called_once() + # One filter for organization, one for rank=1 + # (initial filter is on Release.objects not mock_queryset) + assert mock_queryset.filter.call_count == 2 - @patch("apps.github.api.internal.queries.release.OuterRef") - @patch("apps.github.api.internal.queries.release.Subquery") @patch("apps.github.models.release.Release.objects") def test_recent_releases_distinct_with_all_filters( - self, mock_objects, mock_subquery, mock_outer_ref, mock_queryset, mock_release + self, mock_objects, mock_queryset, mock_release ): """Test distinct filtering with all filters.""" mock_objects.filter.return_value = mock_queryset - mock_queryset.select_related.return_value = mock_queryset - mock_queryset.filter.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_release] - mock_subquery_instance = Mock() - mock_subquery.return_value = mock_subquery_instance - mock_outer_ref_instance = Mock() - mock_outer_ref.return_value = mock_outer_ref_instance - result = ReleaseQuery().recent_releases( distinct=True, login="alice", organization="owasp", limit=5 ) assert result == [mock_release] - mock_subquery.assert_called_once() - mock_outer_ref.assert_called_once_with("author_id") - mock_queryset.select_related.assert_called_once_with( - "author", "repository", "repository__organization" - ) - # Verify both login and organization filters were applied - assert mock_queryset.filter.call_count >= 3 # base filter, login, organization + mock_queryset.annotate.assert_called_once() + # One filter for initial filters (is_draft, is_pre_release, published_at), + # one for login, one for organization, one for rank=1 + # Note: There's also filter() calls for login and organization parameters + mock_queryset.__getitem__.assert_called_with(slice(None, 5)) @patch("apps.github.models.release.Release.objects") def test_recent_releases_login_only(self, mock_objects, mock_queryset, mock_release): """Test filtering releases by login only.""" mock_objects.filter.return_value = mock_queryset - mock_queryset.select_related.return_value = mock_queryset - mock_queryset.filter.return_value = mock_queryset mock_queryset.__getitem__.return_value = [mock_release] result = ReleaseQuery().recent_releases(login="alice") assert result == [mock_release] - mock_queryset.select_related.assert_called_once_with( - "author", "repository", "repository__organization" - ) # Verify login filter was applied assert len(mock_queryset.filter.call_args_list) >= 1 diff --git a/backend/tests/apps/github/api/internal/queries/repository_test.py b/backend/tests/apps/github/api/internal/queries/repository_test.py index f8bb8213c2..3ef0029d97 100644 --- a/backend/tests/apps/github/api/internal/queries/repository_test.py +++ b/backend/tests/apps/github/api/internal/queries/repository_test.py @@ -21,19 +21,20 @@ def mock_repository(self): def test_resolve_repository_existing(self, mock_repository): """Test resolving an existing repository.""" mock_queryset = MagicMock() + mock_queryset.select_related.return_value = mock_queryset mock_queryset.get.return_value = mock_repository with patch( - "apps.github.models.repository.Repository.objects.select_related", - return_value=mock_queryset, - ) as mock_select_related: + "apps.github.models.repository.Repository.objects", + mock_queryset, + ): result = RepositoryQuery().repository( organization_key="test-org", repository_key="test-repo", ) assert result == mock_repository - mock_select_related.assert_called_once_with("organization") + mock_queryset.select_related.assert_called_once_with("organization") mock_queryset.get.assert_called_once_with( organization__login__iexact="test-org", key__iexact="test-repo", @@ -42,19 +43,20 @@ def test_resolve_repository_existing(self, mock_repository): def test_resolve_repository_not_found(self): """Test resolving a non-existent repository.""" mock_queryset = MagicMock() + mock_queryset.select_related.return_value = mock_queryset mock_queryset.get.side_effect = Repository.DoesNotExist with patch( - "apps.github.models.repository.Repository.objects.select_related", - return_value=mock_queryset, - ) as mock_select_related: + "apps.github.models.repository.Repository.objects", + mock_queryset, + ): result = RepositoryQuery().repository( organization_key="non-existent-org", repository_key="non-existent-repo", ) assert result is None - mock_select_related.assert_called_once_with("organization") + mock_queryset.select_related.assert_called_once_with("organization") mock_queryset.get.assert_called_once_with( organization__login__iexact="non-existent-org", key__iexact="non-existent-repo", @@ -68,9 +70,9 @@ def test_resolve_repositories(self, mock_repository): ] with patch( - "apps.github.models.repository.Repository.objects.select_related", - return_value=mock_queryset, - ) as mock_select_related: + "apps.github.models.repository.Repository.objects", + mock_queryset, + ): result = RepositoryQuery().repositories( organization="test-org", limit=1, @@ -78,6 +80,5 @@ def test_resolve_repositories(self, mock_repository): assert isinstance(result, list) assert result[0] == mock_repository - mock_select_related.assert_called_once_with("organization") mock_queryset.filter.assert_called_once_with(organization__login__iexact="test-org") mock_queryset.filter.return_value.order_by.assert_called_once_with("-stars_count") diff --git a/backend/tests/apps/github/models/repository_contributor_test.py b/backend/tests/apps/github/models/repository_contributor_test.py index 1a1d47185d..204750e1bb 100644 --- a/backend/tests/apps/github/models/repository_contributor_test.py +++ b/backend/tests/apps/github/models/repository_contributor_test.py @@ -220,9 +220,10 @@ def test_get_top_contributors_data_processing(self): expected = [ { "avatar_url": "url1", + "contributions_count": 100, + "id": "user1", "login": "user1", "name": "User One", - "contributions_count": 100, } ] assert result == expected diff --git a/backend/tests/apps/github/models/repository_test.py b/backend/tests/apps/github/models/repository_test.py index 07ee59ed01..4ffefc414f 100644 --- a/backend/tests/apps/github/models/repository_test.py +++ b/backend/tests/apps/github/models/repository_test.py @@ -298,17 +298,18 @@ def test_published_releases(self, repository_setup): is_draft=False, is_pre_release=False, published_at__isnull=False ) - def test_recent_milestones(self, mocker, repository_setup): + def test_recent_milestones(self, repository_setup): """Test the recent_milestones property to ensure it returns milestones. ordered by creation date. """ _owner, repository = repository_setup - mock_filter = mocker.patch("apps.github.models.repository.Milestone.objects.filter") - mock_filter.return_value.order_by.return_value = "recent_milestones" - assert repository.recent_milestones == "recent_milestones" - mock_filter.assert_called_with(repository=repository) - mock_filter.return_value.order_by.assert_called_with("-created_at") + with patch.object(Repository, "milestones", new_callable=PropertyMock) as mock_prop: + mock_manager = mock_prop.return_value + mock_manager.order_by.return_value = "recent_milestones" + repository.pk = 123 + assert repository.recent_milestones == "recent_milestones" + mock_manager.order_by.assert_called_with("-created_at") def test_top_languages(self, repository_setup): """Test the top_languages property to ensure it returns a sorted list of. diff --git a/backend/tests/apps/owasp/api/internal/nodes/board_of_directors_test.py b/backend/tests/apps/owasp/api/internal/nodes/board_of_directors_test.py index 6139ac3a5f..6a508e9bee 100644 --- a/backend/tests/apps/owasp/api/internal/nodes/board_of_directors_test.py +++ b/backend/tests/apps/owasp/api/internal/nodes/board_of_directors_test.py @@ -3,27 +3,34 @@ from unittest.mock import Mock from apps.owasp.api.internal.nodes.board_of_directors import BoardOfDirectorsNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestBoardOfDirectorsNode: - def test_node_fields(self): - node = BoardOfDirectorsNode.__strawberry_definition__ - - field_names = {field.name for field in node.fields} +class TestBoardOfDirectorsNode(GraphQLNodeBaseTest): + """Test cases for BoardOfDirectorsNode class.""" - assert "year" in field_names - assert "created_at" in field_names - assert "updated_at" in field_names - assert "owasp_url" in field_names - assert "candidates" in field_names - assert "members" in field_names + def test_node_fields(self): + field_names = { + field.name for field in BoardOfDirectorsNode.__strawberry_definition__.fields + } + expected_field_names = { + "_id", + "candidates", + "created_at", + "members", + "owasp_url", + "updated_at", + "year", + } + assert field_names == expected_field_names def test_owasp_url_resolver(self): """Test owasp_url returns URL from board instance.""" mock_board = Mock() mock_board.owasp_url = "https://board.owasp.org/elections/2025_elections" - result = BoardOfDirectorsNode.owasp_url(mock_board) + field = self._get_field_by_name("owasp_url", BoardOfDirectorsNode) + result = field.base_resolver.wrapped_func(None, mock_board) assert result == "https://board.owasp.org/elections/2025_elections" @@ -35,7 +42,8 @@ def test_candidates_resolver(self): mock_board = Mock() mock_board.get_candidates.return_value = [mock_candidate1, mock_candidate2] - result = BoardOfDirectorsNode.candidates(mock_board) + field = self._get_field_by_name("candidates", BoardOfDirectorsNode) + result = field.base_resolver.wrapped_func(None, mock_board) assert result == [mock_candidate1, mock_candidate2] mock_board.get_candidates.assert_called_once() @@ -48,7 +56,8 @@ def test_members_resolver(self): mock_board = Mock() mock_board.get_members.return_value = [mock_member1, mock_member2] - result = BoardOfDirectorsNode.members(mock_board) + field = self._get_field_by_name("members", BoardOfDirectorsNode) + result = field.base_resolver.wrapped_func(None, mock_board) assert result == [mock_member1, mock_member2] mock_board.get_members.assert_called_once() diff --git a/backend/tests/apps/owasp/api/internal/nodes/chapter_test.py b/backend/tests/apps/owasp/api/internal/nodes/chapter_test.py index f05abd9b80..3b7cb91027 100644 --- a/backend/tests/apps/owasp/api/internal/nodes/chapter_test.py +++ b/backend/tests/apps/owasp/api/internal/nodes/chapter_test.py @@ -1,9 +1,10 @@ """Test cases for ChapterNode.""" from apps.owasp.api.internal.nodes.chapter import ChapterNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestChapterNode: +class TestChapterNode(GraphQLNodeBaseTest): def test_chapter_node_inheritance(self): assert hasattr(ChapterNode, "__strawberry_definition__") @@ -27,38 +28,33 @@ def test_meta_configuration(self): } assert expected_field_names.issubset(field_names) - def _get_field_by_name(self, name): - return next( - (f for f in ChapterNode.__strawberry_definition__.fields if f.name == name), None - ) - def test_resolve_key(self): - field = self._get_field_by_name("key") + field = self._get_field_by_name("key", ChapterNode) assert field is not None assert field.type is str def test_resolve_country(self): - field = self._get_field_by_name("country") + field = self._get_field_by_name("country", ChapterNode) assert field is not None assert field.type is str def test_resolve_region(self): - field = self._get_field_by_name("region") + field = self._get_field_by_name("region", ChapterNode) assert field is not None assert field.type is str def test_resolve_is_active(self): - field = self._get_field_by_name("is_active") + field = self._get_field_by_name("is_active", ChapterNode) assert field is not None assert field.type is bool def test_resolve_contribution_data(self): - field = self._get_field_by_name("contribution_data") + field = self._get_field_by_name("contribution_data", ChapterNode) assert field is not None assert field.type.__class__.__name__ == "NewType" def test_resolve_contribution_stats(self): - field = self._get_field_by_name("contribution_stats") + field = self._get_field_by_name("contribution_stats", ChapterNode) assert field is not None assert field.type.__class__.__name__ == "StrawberryOptional" @@ -78,10 +74,8 @@ def test_contribution_stats_transforms_snake_case_to_camel_case(self): instance = type("BoundNode", (), {})() instance.contribution_stats = mock_chapter.contribution_stats - field = self._get_field_by_name("contribution_stats") - resolver = field.base_resolver.wrapped_func - - result = resolver(instance) + field = self._get_field_by_name("contribution_stats", ChapterNode) + result = field.base_resolver.wrapped_func(None, instance) assert result is not None assert result["commits"] == 75 diff --git a/backend/tests/apps/owasp/api/internal/nodes/committee_test.py b/backend/tests/apps/owasp/api/internal/nodes/committee_test.py index 8596311673..081348e9b4 100644 --- a/backend/tests/apps/owasp/api/internal/nodes/committee_test.py +++ b/backend/tests/apps/owasp/api/internal/nodes/committee_test.py @@ -4,9 +4,10 @@ from unittest.mock import Mock from apps.owasp.api.internal.nodes.committee import CommitteeNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestCommitteeNode: +class TestCommitteeNode(GraphQLNodeBaseTest): def test_contributors_count_resolver(self): """Test contributors_count returns count from repository.""" mock_repo = Mock() @@ -15,7 +16,8 @@ def test_contributors_count_resolver(self): mock_committee = Mock() mock_committee.owasp_repository = mock_repo - result = CommitteeNode.contributors_count(mock_committee) + field = self._get_field_by_name("contributors_count", CommitteeNode) + result = field.base_resolver.wrapped_func(None, mock_committee) assert result == 42 @@ -24,7 +26,8 @@ def test_created_at_resolver(self): mock_committee = Mock() mock_committee.idx_created_at = 1234567890.0 - result = CommitteeNode.created_at(mock_committee) + field = self._get_field_by_name("created_at", CommitteeNode) + result = field.base_resolver.wrapped_func(None, mock_committee) assert math.isclose(result, 1234567890.0) @@ -36,7 +39,8 @@ def test_forks_count_resolver(self): mock_committee = Mock() mock_committee.owasp_repository = mock_repo - result = CommitteeNode.forks_count(mock_committee) + field = self._get_field_by_name("forks_count", CommitteeNode) + result = field.base_resolver.wrapped_func(None, mock_committee) assert result == 15 @@ -48,7 +52,8 @@ def test_issues_count_resolver(self): mock_committee = Mock() mock_committee.owasp_repository = mock_repo - result = CommitteeNode.issues_count(mock_committee) + field = self._get_field_by_name("issues_count", CommitteeNode) + result = field.base_resolver.wrapped_func(None, mock_committee) assert result == 23 @@ -56,7 +61,8 @@ def test_repositories_count_resolver(self): """Test repositories_count always returns 1 for committees.""" mock_committee = Mock() - result = CommitteeNode.repositories_count(mock_committee) + field = self._get_field_by_name("repositories_count", CommitteeNode) + result = field.base_resolver.wrapped_func(None, mock_committee) assert result == 1 @@ -68,6 +74,7 @@ def test_stars_count_resolver(self): mock_committee = Mock() mock_committee.owasp_repository = mock_repo - result = CommitteeNode.stars_count(mock_committee) + field = self._get_field_by_name("stars_count", CommitteeNode) + result = field.base_resolver.wrapped_func(None, mock_committee) assert result == 100 diff --git a/backend/tests/apps/owasp/api/internal/nodes/common_test.py b/backend/tests/apps/owasp/api/internal/nodes/common_test.py index 4a9d18e0f2..0592ea71ff 100644 --- a/backend/tests/apps/owasp/api/internal/nodes/common_test.py +++ b/backend/tests/apps/owasp/api/internal/nodes/common_test.py @@ -15,7 +15,7 @@ def test_entity_leaders_resolver(self): mock_entity = Mock() mock_entity.entity_leaders = [mock_leader1, mock_leader2] - result = GenericEntityNode.entity_leaders(mock_entity) + result = GenericEntityNode().entity_leaders(mock_entity) assert result == [mock_leader1, mock_leader2] @@ -24,16 +24,16 @@ def test_leaders_resolver(self): mock_entity = Mock() mock_entity.idx_leaders = ["leader1", "leader2"] - result = GenericEntityNode.leaders(mock_entity) + result = GenericEntityNode.leaders(None, mock_entity) assert result == ["leader1", "leader2"] def test_related_urls_resolver(self): - """Test related_urls returns indexed URLs list.""" + """Test related_urls returns URLs list.""" mock_entity = Mock() - mock_entity.idx_related_urls = ["https://example.com", "https://test.com"] + mock_entity.related_urls = ["https://example.com", "https://test.com"] - result = GenericEntityNode.related_urls(mock_entity) + result = GenericEntityNode.related_urls(None, mock_entity) assert result == ["https://example.com", "https://test.com"] @@ -42,7 +42,7 @@ def test_updated_at_resolver(self): mock_entity = Mock() mock_entity.idx_updated_at = 1234567890.0 - result = GenericEntityNode.updated_at(mock_entity) + result = GenericEntityNode.updated_at(None, mock_entity) assert math.isclose(result, 1234567890.0) @@ -51,6 +51,6 @@ def test_url_resolver(self): mock_entity = Mock() mock_entity.idx_url = "https://owasp.org/www-project-example" - result = GenericEntityNode.url(mock_entity) + result = GenericEntityNode.url(None, mock_entity) assert result == "https://owasp.org/www-project-example" diff --git a/backend/tests/apps/owasp/api/internal/nodes/member_snapshot_test.py b/backend/tests/apps/owasp/api/internal/nodes/member_snapshot_test.py index bbca8651d2..33abbc4829 100644 --- a/backend/tests/apps/owasp/api/internal/nodes/member_snapshot_test.py +++ b/backend/tests/apps/owasp/api/internal/nodes/member_snapshot_test.py @@ -3,9 +3,10 @@ from unittest.mock import Mock from apps.owasp.api.internal.nodes.member_snapshot import MemberSnapshotNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestMemberSnapshotNode: +class TestMemberSnapshotNode(GraphQLNodeBaseTest): def test_node_fields(self): mock_snapshot = Mock() mock_snapshot.start_at = "2025-01-01" @@ -31,7 +32,8 @@ def test_commits_count_resolver(self): mock_snapshot = Mock() mock_snapshot.commits_count = 42 - result = MemberSnapshotNode.commits_count(mock_snapshot) + field = self._get_field_by_name("commits_count", MemberSnapshotNode) + result = field.base_resolver.wrapped_func(None, mock_snapshot) assert result == 42 @@ -41,7 +43,8 @@ def test_github_user_resolver(self): mock_snapshot = Mock() mock_snapshot.github_user = mock_user - result = MemberSnapshotNode.github_user(mock_snapshot) + field = self._get_field_by_name("github_user", MemberSnapshotNode) + result = field.base_resolver.wrapped_func(None, mock_snapshot) assert result == mock_user @@ -50,7 +53,8 @@ def test_issues_count_resolver(self): mock_snapshot = Mock() mock_snapshot.issues_count = 15 - result = MemberSnapshotNode.issues_count(mock_snapshot) + field = self._get_field_by_name("issues_count", MemberSnapshotNode) + result = field.base_resolver.wrapped_func(None, mock_snapshot) assert result == 15 @@ -59,7 +63,8 @@ def test_pull_requests_count_resolver(self): mock_snapshot = Mock() mock_snapshot.pull_requests_count = 23 - result = MemberSnapshotNode.pull_requests_count(mock_snapshot) + field = self._get_field_by_name("pull_requests_count", MemberSnapshotNode) + result = field.base_resolver.wrapped_func(None, mock_snapshot) assert result == 23 @@ -68,7 +73,8 @@ def test_messages_count_resolver(self): mock_snapshot = Mock() mock_snapshot.messages_count = 100 - result = MemberSnapshotNode.messages_count(mock_snapshot) + field = self._get_field_by_name("messages_count", MemberSnapshotNode) + result = field.base_resolver.wrapped_func(None, mock_snapshot) assert result == 100 @@ -77,6 +83,7 @@ def test_total_contributions_resolver(self): mock_snapshot = Mock() mock_snapshot.total_contributions = 80 - result = MemberSnapshotNode.total_contributions(mock_snapshot) + field = self._get_field_by_name("total_contributions", MemberSnapshotNode) + result = field.base_resolver.wrapped_func(None, mock_snapshot) assert result == 80 diff --git a/backend/tests/apps/owasp/api/internal/nodes/project_health_metrics_test.py b/backend/tests/apps/owasp/api/internal/nodes/project_health_metrics_test.py index 03f3f672e8..4f282b4c18 100644 --- a/backend/tests/apps/owasp/api/internal/nodes/project_health_metrics_test.py +++ b/backend/tests/apps/owasp/api/internal/nodes/project_health_metrics_test.py @@ -5,9 +5,10 @@ import pytest from apps.owasp.api.internal.nodes.project_health_metrics import ProjectHealthMetricsNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestProjectHealthMetricsNode: +class TestProjectHealthMetricsNode(GraphQLNodeBaseTest): def test_project_health_metrics_node_inheritance(self): assert hasattr(ProjectHealthMetricsNode, "__strawberry_definition__") @@ -42,16 +43,6 @@ def test_meta_configuration(self): } assert expected_field_names.issubset(field_names) - def _get_field_by_name(self, name): - return next( - ( - f - for f in ProjectHealthMetricsNode.__strawberry_definition__.fields - if f.name == name - ), - None, - ) - @pytest.mark.parametrize( ("field_name", "expected_type"), [ @@ -80,6 +71,6 @@ def _get_field_by_name(self, name): ], ) def test_field_types(self, field_name, expected_type): - field = self._get_field_by_name(field_name) + field = self._get_field_by_name(field_name, ProjectHealthMetricsNode) assert field is not None assert field.type is expected_type diff --git a/backend/tests/apps/owasp/api/internal/nodes/project_health_stats_test.py b/backend/tests/apps/owasp/api/internal/nodes/project_health_stats_test.py index 3bcd9c1caa..28bd26c81a 100644 --- a/backend/tests/apps/owasp/api/internal/nodes/project_health_stats_test.py +++ b/backend/tests/apps/owasp/api/internal/nodes/project_health_stats_test.py @@ -6,9 +6,10 @@ from strawberry.types.base import StrawberryList from apps.owasp.api.internal.nodes.project_health_stats import ProjectHealthStatsNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestHealthStatsNode: +class TestHealthStatsNode(GraphQLNodeBaseTest): def test_project_health_stats_node_inheritance(self): assert hasattr(ProjectHealthStatsNode, "__strawberry_definition__") @@ -32,20 +33,9 @@ def test_meta_configuration(self): } assert expected_field_names == field_names - def _get_field_by_name(self, name): - return next( - ( - field - for field in ProjectHealthStatsNode.__strawberry_definition__.fields - if field.name == name - ), - None, - ) - @pytest.mark.parametrize( ("field_name", "expected_type"), [ - ("average_score", float), ("monthly_overall_scores", list[float]), ("monthly_overall_scores_months", list[int]), ("projects_count_healthy", int), @@ -60,7 +50,7 @@ def _get_field_by_name(self, name): ], ) def test_field_types(self, field_name, expected_type): - field = self._get_field_by_name(field_name) + field = self._get_field_by_name(field_name, ProjectHealthStatsNode) assert field is not None, f"Field {field_name} not found in HealthStatsNode." origin = get_origin(expected_type) diff --git a/backend/tests/apps/owasp/api/internal/nodes/project_test.py b/backend/tests/apps/owasp/api/internal/nodes/project_test.py index d47cb64287..4a57206e31 100644 --- a/backend/tests/apps/owasp/api/internal/nodes/project_test.py +++ b/backend/tests/apps/owasp/api/internal/nodes/project_test.py @@ -7,9 +7,10 @@ from apps.github.api.internal.nodes.repository import RepositoryNode from apps.owasp.api.internal.nodes.project import ProjectNode from apps.owasp.api.internal.nodes.project_health_metrics import ProjectHealthMetricsNode +from tests.apps.common.graphql_node_base_test import GraphQLNodeBaseTest -class TestProjectNode: +class TestProjectNode(GraphQLNodeBaseTest): def test_project_node_inheritance(self): assert hasattr(ProjectNode, "__strawberry_definition__") @@ -41,78 +42,73 @@ def test_meta_configuration(self): } assert expected_field_names.issubset(field_names) - def _get_field_by_name(self, name): - return next( - (f for f in ProjectNode.__strawberry_definition__.fields if f.name == name), None - ) - def test_resolve_health_metrics_latest(self): - field = self._get_field_by_name("health_metrics_latest") + field = self._get_field_by_name("health_metrics_latest", ProjectNode) assert field is not None assert field.type.of_type is ProjectHealthMetricsNode def test_resolve_health_metrics_list(self): - field = self._get_field_by_name("health_metrics_list") + field = self._get_field_by_name("health_metrics_list", ProjectNode) assert field is not None assert field.type.of_type is ProjectHealthMetricsNode def test_resolve_issues_count(self): - field = self._get_field_by_name("issues_count") + field = self._get_field_by_name("issues_count", ProjectNode) assert field is not None assert field.type is int def test_resolve_key(self): - field = self._get_field_by_name("key") + field = self._get_field_by_name("key", ProjectNode) assert field is not None assert field.type is str def test_resolve_languages(self): - field = self._get_field_by_name("languages") + field = self._get_field_by_name("languages", ProjectNode) assert field is not None assert field.type == list[str] def test_resolve_recent_issues(self): - field = self._get_field_by_name("recent_issues") + field = self._get_field_by_name("recent_issues", ProjectNode) assert field is not None assert field.type.of_type is IssueNode def test_resolve_recent_milestones(self): - field = self._get_field_by_name("recent_milestones") + field = self._get_field_by_name("recent_milestones", ProjectNode) assert field is not None assert field.type.of_type is MilestoneNode def test_resolve_recent_pull_requests(self): - field = self._get_field_by_name("recent_pull_requests") + field = self._get_field_by_name("recent_pull_requests", ProjectNode) assert field is not None assert field.type.of_type is PullRequestNode def test_resolve_recent_releases(self): - field = self._get_field_by_name("recent_releases") + field = self._get_field_by_name("recent_releases", ProjectNode) assert field is not None assert field.type.of_type is ReleaseNode def test_resolve_repositories(self): - field = self._get_field_by_name("repositories") + field = self._get_field_by_name("repositories", ProjectNode) assert field is not None assert field.type.of_type is RepositoryNode def test_resolve_repositories_count(self): - field = self._get_field_by_name("repositories_count") + field = self._get_field_by_name("repositories_count", ProjectNode) assert field is not None assert field.type is int def test_resolve_topics(self): - field = self._get_field_by_name("topics") + field = self._get_field_by_name("topics", ProjectNode) assert field is not None assert field.type == list[str] def test_resolve_contribution_stats(self): - field = self._get_field_by_name("contribution_stats") + field = self._get_field_by_name("contribution_stats", ProjectNode) assert field is not None assert field.type.__class__.__name__ == "StrawberryOptional" def test_resolve_contribution_data(self): - field = self._get_field_by_name("contribution_data") + field = self._get_field_by_name("contribution_data", ProjectNode) assert field is not None assert field.type.__class__.__name__ == "StrawberryOptional" @@ -132,10 +128,8 @@ def test_contribution_stats_transforms_snake_case_to_camel_case(self): instance = type("BoundNode", (), {})() instance.contribution_stats = mock_project.contribution_stats - field = self._get_field_by_name("contribution_stats") - resolver = field.base_resolver.wrapped_func - - result = resolver(instance) + field = self._get_field_by_name("contribution_stats", ProjectNode) + result = field.base_resolver.wrapped_func(None, instance) assert result is not None assert result["commits"] == 100 diff --git a/backend/tests/apps/owasp/api/internal/queries/member_snapshot_test.py b/backend/tests/apps/owasp/api/internal/queries/member_snapshot_test.py index ece3e985bf..ad87ae500b 100644 --- a/backend/tests/apps/owasp/api/internal/queries/member_snapshot_test.py +++ b/backend/tests/apps/owasp/api/internal/queries/member_snapshot_test.py @@ -33,17 +33,21 @@ def test_member_snapshot_by_user_and_year(self): mock_user_cls.objects.get.return_value = mock_user mock_queryset = Mock() + mock_queryset.select_related.return_value = mock_queryset + mock_queryset.prefetch_related.return_value = mock_queryset mock_queryset.filter.return_value = mock_queryset mock_queryset.order_by.return_value = mock_queryset mock_queryset.first.return_value = mock_snapshot - mock_snapshot_cls.objects.filter.return_value = mock_queryset + mock_snapshot_cls.objects = mock_queryset result = query.member_snapshot(user_login="testuser", start_year=2025) mock_user_cls.objects.get.assert_called_once_with(login="testuser") - mock_snapshot_cls.objects.filter.assert_called_once_with(github_user=mock_user) - mock_queryset.filter.assert_called_once_with(start_at__year=2025) + # select_related and prefetch_related are called, then filter twice + mock_queryset.select_related.assert_called_once() + mock_queryset.prefetch_related.assert_called_once() + assert mock_queryset.filter.call_count == 2 assert result == mock_snapshot def test_member_snapshots_all(self): @@ -55,6 +59,7 @@ def test_member_snapshots_all(self): "apps.owasp.api.internal.queries.member_snapshot.MemberSnapshot" ) as mock_snapshot_cls: mock_queryset = Mock() + mock_queryset.select_related.return_value = mock_queryset mock_queryset.order_by.return_value = mock_queryset mock_queryset.__getitem__ = Mock(return_value=mock_snapshots) @@ -63,6 +68,7 @@ def test_member_snapshots_all(self): result = query.member_snapshots(limit=10) mock_snapshot_cls.objects.all.assert_called_once() + mock_queryset.select_related.assert_called_once_with("github_user") mock_queryset.order_by.assert_called_once_with("-start_at") mock_queryset.__getitem__.assert_called_once_with(slice(None, 10, None)) assert result == mock_snapshots @@ -82,6 +88,7 @@ def test_member_snapshots_by_user(self): mock_user_cls.objects.get.return_value = mock_user mock_queryset = Mock() + mock_queryset.select_related.return_value = mock_queryset mock_queryset.filter.return_value = mock_queryset mock_queryset.order_by.return_value = mock_queryset mock_queryset.__getitem__ = Mock(return_value=mock_snapshots) @@ -91,6 +98,7 @@ def test_member_snapshots_by_user(self): result = query.member_snapshots(user_login="testuser", limit=5) mock_user_cls.objects.get.assert_called_once_with(login="testuser") + mock_queryset.select_related.assert_called_once_with("github_user") mock_queryset.filter.assert_called_once_with(github_user=mock_user) assert result == mock_snapshots diff --git a/backend/tests/apps/owasp/models/chapter_test.py b/backend/tests/apps/owasp/models/chapter_test.py index 92e62af8fd..0c09445a7e 100644 --- a/backend/tests/apps/owasp/models/chapter_test.py +++ b/backend/tests/apps/owasp/models/chapter_test.py @@ -113,10 +113,15 @@ def test_str_representation(self, name, key, expected_str): ], ) def test_active_chapters_count(self, value): - with patch("apps.common.index.IndexBase.get_total_count") as mock_count: - mock_count.return_value = value + with patch.object(Chapter.objects, "filter") as mock_filter: + mock_filter.return_value.count.return_value = value assert Chapter.active_chapters_count() == value - mock_count.assert_called_once_with("chapters", search_filters="idx_is_active:true") + mock_filter.assert_called_once_with( + is_active=True, + latitude__isnull=False, + longitude__isnull=False, + owasp_repository__is_empty=False, + ) @pytest.mark.parametrize( ("has_suggested_location", "has_coordinates"), diff --git a/backend/tests/apps/owasp/models/committee_test.py b/backend/tests/apps/owasp/models/committee_test.py index 59454e3468..dd006c50d0 100644 --- a/backend/tests/apps/owasp/models/committee_test.py +++ b/backend/tests/apps/owasp/models/committee_test.py @@ -2,7 +2,6 @@ import pytest -from apps.common.index import IndexBase from apps.github.models.repository import Repository from apps.github.models.user import User from apps.owasp.models.committee import Committee @@ -27,10 +26,11 @@ def test_str_representation(self, name, expected_str): ], ) def test_active_committees_count(self, value): - with patch.object(IndexBase, "get_total_count", return_value=value) as mock_count: + with patch.object(Committee.objects, "filter") as mock_filter: + mock_filter.return_value.count.return_value = value count = Committee.active_committees_count() assert count == value - mock_count.assert_called_once_with("committees") + mock_filter.assert_called_once_with(has_active_repositories=True) @pytest.mark.parametrize( ("summary"), diff --git a/backend/tests/fuzz/__init__.py b/backend/tests/fuzz/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/tests/fuzz/graphql_test.py b/backend/tests/fuzz/graphql_test.py new file mode 100644 index 0000000000..f006395591 --- /dev/null +++ b/backend/tests/fuzz/graphql_test.py @@ -0,0 +1,33 @@ +"""OWASP Nest GraphQL API fuzz tests.""" + +import logging +import os +from contextlib import suppress + +import schemathesis +from schemathesis.graphql.checks import GraphQLClientError + +if not (BASE_URL := os.getenv("BASE_URL")) or not (CSRF_TOKEN := os.getenv("CSRF_TOKEN")): + message = "BASE_URL and CSRF_TOKEN must be set in the environment." + raise ValueError(message) + +HEADERS = { + "Cookie": f"csrftoken={CSRF_TOKEN}", + "X-CSRFToken": CSRF_TOKEN, +} + +logger = logging.getLogger(__name__) +schema = schemathesis.graphql.from_url( + f"{BASE_URL}/graphql/", + headers=HEADERS, + timeout=30, + wait_for_schema=10, +) + + +@schema.parametrize() +def test_graphql_api(case: schemathesis.Case) -> None: + """Test GraphQL API endpoints.""" + logger.info(case.as_curl_command()) + with suppress(GraphQLClientError): + case.call_and_validate(headers=HEADERS) diff --git a/backend/tests/fuzz/rest_test.py b/backend/tests/fuzz/rest_test.py new file mode 100644 index 0000000000..bd03dc976b --- /dev/null +++ b/backend/tests/fuzz/rest_test.py @@ -0,0 +1,30 @@ +"""OWASP Nest REST API fuzz tests.""" + +import logging +import os + +import schemathesis + +if not (REST_URL := os.getenv("REST_URL")) or not (CSRF_TOKEN := os.getenv("CSRF_TOKEN")): + message = "REST_URL and CSRF_TOKEN must be set in the environment." + raise ValueError(message) + +HEADERS = { + "Cookie": f"csrftoken={CSRF_TOKEN}", + "X-CSRFToken": CSRF_TOKEN, +} + +logger = logging.getLogger(__name__) +schema = schemathesis.openapi.from_url( + f"{REST_URL}/openapi.json", + headers=HEADERS, + timeout=30, + wait_for_schema=10, +) + + +@schema.parametrize() +def test_rest_api(case): + """Test REST API endpoints.""" + logger.info(case.as_curl_command()) + case.call_and_validate(headers=HEADERS) diff --git a/cspell/Makefile b/cspell/Makefile index f1d65864bd..441568ea57 100644 --- a/cspell/Makefile +++ b/cspell/Makefile @@ -1,11 +1,10 @@ check-spelling: cspell-check cspell-install: - # Check if cspell:ci image exists (from CI build step), tag it to 'cspell' if found, - # otherwise build the image from scratch for local development. + @ # Check if cspell:ci image exists (from CI build step), tag it to 'cspell' if found, + @ # otherwise build the image from scratch for local development. @docker image inspect cspell:ci >/dev/null 2>&1 && \ - docker tag cspell:ci cspell || \ - docker build -t cspell cspell + @docker tag cspell:ci cspell || docker build -t cspell cspell cspell-check: CMD="--no-progress -r /nest" cspell-check: cspell-install cspell-run diff --git a/cspell/custom-dict.txt b/cspell/custom-dict.txt index 5c39d84ac4..e2d9cc1bfb 100644 --- a/cspell/custom-dict.txt +++ b/cspell/custom-dict.txt @@ -2,6 +2,7 @@ Agentic Agsoc Aichi Aissue +Atqc Aupdated BOTTOMPADDING CCSP @@ -21,6 +22,7 @@ NOASSERTION NOSONAR Nadu Nominatim +PGPASSWORD PLR PYTHONPATH PYTHONUNBUFFERED @@ -32,6 +34,7 @@ Sonarqube Stegen T04T40NHX Truncator +Turbopack Twitterbot WSL Whistleblower @@ -49,6 +52,7 @@ aquasecurity arithmatex arkid15r askowasp +attisdropped bangbang bsky certbot @@ -67,6 +71,7 @@ dockerhub dsn elevenlabs env +euo facebookexternalhit gamesec geocoders @@ -74,6 +79,7 @@ geoloc geopy gha graphiql +graphqler gunicorn hackathon hcl @@ -116,7 +122,9 @@ nestbot ngx noinput nosniff +nspname numfmt +openblas openstreetmap owasppcitoolkit owtf @@ -142,6 +150,7 @@ rsc saft sakanashi samm +schemathesis seo skillstruck slackbot @@ -149,7 +158,9 @@ slideshare speakerdeck superfences tfbackend +tgz tiktok +trgm trivyignores tsc unassigning @@ -164,6 +175,7 @@ xdg xdist xoxb xsser +xzf zapconfig zaproxy zsc diff --git a/docker-compose/e2e/compose.yaml b/docker-compose/e2e/compose.yaml new file mode 100644 index 0000000000..1f808eaf09 --- /dev/null +++ b/docker-compose/e2e/compose.yaml @@ -0,0 +1,113 @@ +services: + backend: + container_name: e2e-nest-backend + command: > + sh -c ' + python manage.py migrate && + gunicorn wsgi:application --bind 0.0.0.0:9000 + ' + build: + context: ../../backend + dockerfile: ../docker/backend/Dockerfile + depends_on: + db: + condition: service_healthy + cache: + condition: service_healthy + env_file: ../../backend/.env.e2e.example + networks: + - e2e-nest-network + ports: + - 9000:9000 + healthcheck: + interval: 10s + retries: 10 + test: > + sh -c ' + wget --spider http://backend:9000/a/ + ' + timeout: 10s + start_period: 5s + + data-loader: + container_name: e2e-nest-data-loader + image: pgvector/pgvector:pg16 + depends_on: + db: + condition: service_healthy + backend: + condition: service_healthy + environment: + PGPASSWORD: ${DJANGO_DB_PASSWORD:-nest_user_e2e_password} + POSTGRES_USER: ${DJANGO_DB_USER:-nest_user_e2e} + POSTGRES_DB: ${DJANGO_DB_NAME:-nest_db_e2e} + volumes: + - ../../backend/data:/data:ro + networks: + - e2e-nest-network + command: > + sh -c ' + echo "Loading data from dump..." && + pg_restore -h db -U $$POSTGRES_USER -d $$POSTGRES_DB /data/nest.dump && + echo "Data loading completed." + ' + db: + container_name: e2e-nest-db + image: pgvector/pgvector:pg16 + environment: + POSTGRES_DB: ${DJANGO_DB_NAME:-nest_db_e2e} + POSTGRES_PASSWORD: ${DJANGO_DB_PASSWORD:-nest_user_e2e_password} + POSTGRES_USER: ${DJANGO_DB_USER:-nest_user_e2e} + healthcheck: + interval: 5s + retries: 5 + test: [CMD-SHELL, pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -h localhost -p 5432] + timeout: 5s + networks: + - e2e-nest-network + volumes: + - e2e-db-data:/var/lib/postgresql/data + ports: + - 5433:5432 + + cache: + command: > + sh -c ' + redis-server --requirepass $$REDIS_PASSWORD --maxmemory 100mb --maxmemory-policy allkeys-lru + ' + container_name: e2e-nest-cache + image: redis:8.0.5-alpine3.21 + environment: + REDIS_PASSWORD: ${DJANGO_REDIS_PASSWORD:-nest-cache-e2e-password} + healthcheck: + interval: 5s + retries: 5 + test: [CMD, redis-cli, -a, $$REDIS_PASSWORD, ping] + timeout: 5s + networks: + - e2e-nest-network + volumes: + - e2e-cache-data:/data + + e2e-tests: + container_name: e2e-nest-tests + build: + context: ../../frontend + dockerfile: ../docker/frontend/Dockerfile.e2e.test + command: > + sh -c ' + pnpm run test:e2e + ' + depends_on: + backend: + condition: service_healthy + env_file: ../../frontend/.env.e2e.example + networks: + - e2e-nest-network + +networks: + e2e-nest-network: + +volumes: + e2e-cache-data: + e2e-db-data: diff --git a/docker-compose/fuzz/compose.yaml b/docker-compose/fuzz/compose.yaml new file mode 100644 index 0000000000..4d5b0bdca3 --- /dev/null +++ b/docker-compose/fuzz/compose.yaml @@ -0,0 +1,126 @@ +services: + backend: + container_name: fuzz-nest-backend + command: > + sh -c ' + python manage.py migrate && + gunicorn wsgi:application --bind 0.0.0.0:9500 + ' + build: + context: ../../backend + dockerfile: ../docker/backend/Dockerfile + depends_on: + db: + condition: service_healthy + cache: + condition: service_healthy + env_file: ../../backend/.env.fuzz.example + networks: + - fuzz-nest-network + ports: + - 9500:9500 + healthcheck: + interval: 10s + retries: 10 + test: > + sh -c ' + wget --spider http://backend:9500/a/ + ' + timeout: 10s + start_period: 5s + + data-loader: + container_name: fuzz-nest-data-loader + image: pgvector/pgvector:pg16 + depends_on: + db: + condition: service_healthy + backend: + condition: service_healthy + environment: + PGPASSWORD: ${DJANGO_DB_PASSWORD:-nest_user_fuzz_password} + POSTGRES_USER: ${DJANGO_DB_USER:-nest_user_fuzz} + POSTGRES_DB: ${DJANGO_DB_NAME:-nest_db_fuzz} + volumes: + - ../../backend/data:/data:ro + networks: + - fuzz-nest-network + command: > + sh -c ' + echo "Loading data from dump..." && + pg_restore -h db -U $$POSTGRES_USER -d $$POSTGRES_DB /data/nest.dump && + echo "Data loading completed." + ' + db: + container_name: fuzz-nest-db + image: pgvector/pgvector:pg16 + environment: + POSTGRES_DB: ${DJANGO_DB_NAME:-nest_db_fuzz} + POSTGRES_PASSWORD: ${DJANGO_DB_PASSWORD:-nest_user_fuzz_password} + POSTGRES_USER: ${DJANGO_DB_USER:-nest_user_fuzz} + healthcheck: + interval: 5s + retries: 5 + test: [CMD-SHELL, pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -h localhost -p 5432] + timeout: 5s + networks: + - fuzz-nest-network + volumes: + - fuzz-db-data:/var/lib/postgresql/data + ports: + - 5434:5432 + + cache: + command: > + sh -c ' + redis-server --requirepass $$REDIS_PASSWORD --maxmemory 100mb --maxmemory-policy allkeys-lru + ' + container_name: fuzz-nest-cache + image: redis:8.0.5-alpine3.21 + environment: + REDIS_PASSWORD: ${DJANGO_REDIS_PASSWORD:-nest-fuzz-cache-password} + healthcheck: + interval: 5s + retries: 5 + test: [CMD, redis-cli, -a, $$REDIS_PASSWORD, ping] + timeout: 5s + networks: + - fuzz-nest-network + volumes: + - fuzz-cache-data:/data + + graphql: + container_name: fuzz-nest-graphql + build: + context: ../../backend + dockerfile: ../docker/backend/Dockerfile.fuzz + environment: + BASE_URL: http://backend:9500 + TEST_FILE: graphql_test.py + depends_on: + backend: + condition: service_healthy + networks: + - fuzz-nest-network + + rest: + container_name: fuzz-nest-rest + build: + context: ../../backend + dockerfile: ../docker/backend/Dockerfile.fuzz + environment: + BASE_URL: http://backend:9500 + REST_URL: http://backend:9500/api/v0 + TEST_FILE: rest_test.py + depends_on: + backend: + condition: service_healthy + networks: + - fuzz-nest-network + +networks: + fuzz-nest-network: + +volumes: + fuzz-cache-data: + fuzz-db-data: diff --git a/docker-compose/production/compose.yaml b/docker-compose/production/compose.yaml index ed79a179e8..fd49e69444 100644 --- a/docker-compose/production/compose.yaml +++ b/docker-compose/production/compose.yaml @@ -1,6 +1,7 @@ services: production-nest-backend: container_name: production-nest-backend + entrypoint: /home/owasp/entrypoint.sh image: owasp/nest:backend-production env_file: .env.backend depends_on: diff --git a/docker-compose/staging/compose.yaml b/docker-compose/staging/compose.yaml index b2a8161a85..051ce8433d 100644 --- a/docker-compose/staging/compose.yaml +++ b/docker-compose/staging/compose.yaml @@ -1,6 +1,7 @@ services: staging-nest-backend: container_name: staging-nest-backend + entrypoint: /home/owasp/entrypoint.sh image: owasp/nest:backend-staging env_file: .env.backend depends_on: diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 4bde6c0821..a4a609955d 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -65,5 +65,3 @@ RUN rm -rf /home/owasp/.cache && \ chmod +x /home/owasp/entrypoint.sh USER owasp - -CMD ["/home/owasp/entrypoint.sh"] diff --git a/docker/backend/Dockerfile.fuzz b/docker/backend/Dockerfile.fuzz new file mode 100644 index 0000000000..75d06b16dd --- /dev/null +++ b/docker/backend/Dockerfile.fuzz @@ -0,0 +1,32 @@ +FROM python:3.13.7-alpine AS builder + +SHELL ["/bin/sh", "-o", "pipefail", "-c"] + +ENV PIP_CACHE_DIR="/home/owasp/.cache/pip" \ + PYTHONUNBUFFERED=1 \ + APK_CACHE_DIR="/home/owasp/.cache/apk-fuzz-stage" \ + APK_SYMLINK_DIR="/etc/apk/cache" \ + FORCE_COLOR=1 + +RUN mkdir -p ${APK_CACHE_DIR} && \ + ln -s ${APK_CACHE_DIR} ${APK_SYMLINK_DIR} + +RUN --mount=type=cache,target=${APK_CACHE_DIR} \ + apk update && apk upgrade && \ + apk add curl jq && \ + addgroup -S owasp && \ + adduser -S -h /home/owasp -G owasp owasp + +RUN --mount=type=cache,target=${PIP_CACHE_DIR} \ + python -m pip install --upgrade pip && \ + pip install schemathesis --cache-dir ${PIP_CACHE_DIR} + +WORKDIR /home/owasp + +COPY --chown=root:root --chmod=755 ./entrypoint.fuzz.sh ./entrypoint.sh +COPY --chown=root:root --chmod=755 tests/fuzz tests + +RUN chmod +x ./entrypoint.sh + +USER owasp +ENTRYPOINT ["./entrypoint.sh"] diff --git a/docker/backend/Dockerfile.local b/docker/backend/Dockerfile.local index ec108faaab..84994c2807 100644 --- a/docker/backend/Dockerfile.local +++ b/docker/backend/Dockerfile.local @@ -46,7 +46,7 @@ RUN mkdir -p ${APK_CACHE_DIR} && \ RUN --mount=type=cache,target=${APK_CACHE_DIR} \ apk update && apk upgrade && \ - apk add postgresql-client redis && \ + apk add postgresql16-client redis && \ addgroup -S owasp && \ adduser -S -h /home/owasp -G owasp owasp diff --git a/docker/backend/Dockerfile.test b/docker/backend/Dockerfile.test index f2b5c73256..f105bc1e2e 100644 --- a/docker/backend/Dockerfile.test +++ b/docker/backend/Dockerfile.test @@ -34,7 +34,9 @@ COPY manage.py wsgi.py ./ COPY settings settings COPY static static COPY templates templates -COPY tests tests +COPY tests/apps tests/apps +COPY tests/__init__.py tests/__init__.py +COPY tests/conftest.py tests/conftest.py FROM python:3.13.7-alpine diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index 9106c60da7..ebe729236f 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -21,6 +21,7 @@ RUN --mount=type=cache,target=${APK_CACHE_DIR} \ WORKDIR /app RUN --mount=type=cache,target=${NPM_CACHE} \ + npm install --ignore-scripts -g npm@latest --cache ${NPM_CACHE} && \ npm install --ignore-scripts -g pnpm --cache ${NPM_CACHE} COPY --chmod=444 package.json pnpm-lock.yaml ./ @@ -47,6 +48,18 @@ WORKDIR /app ENV NEXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production +# Fix CVE-2026-23745: Update npm's bundled tar from 7.5.1 to 7.5.3 in runner stage +# Note: Must download tar with npm pack BEFORE removing the old tar (npm needs it) +RUN cd /tmp && \ + npm pack tar@7.5.3 && \ + tar -xzf tar-7.5.3.tgz && \ + TAR_DIR="/usr/local/lib/node_modules/npm/node_modules/tar" && \ + rm -rf "${TAR_DIR}" && \ + cp -r package "${TAR_DIR}" && \ + chmod -R 755 "${TAR_DIR}" && \ + rm -rf package tar-7.5.3.tgz && \ + grep -q 'version.*7.5.3' "${TAR_DIR}/package.json" + RUN addgroup --system --gid 1001 nodejs && \ adduser --system --uid 1001 -G nodejs nextjs # Copying files with root as owner, so that executing user cannot change the container. diff --git a/frontend/.env.e2e.example b/frontend/.env.e2e.example new file mode 100644 index 0000000000..aec00ea3a7 --- /dev/null +++ b/frontend/.env.e2e.example @@ -0,0 +1,16 @@ +NEXTAUTH_SECRET= +NEXTAUTH_URL=http://localhost:3000/ +NEXT_PUBLIC_API_URL=http://localhost:9000/ +NEXT_PUBLIC_CSRF_URL=http://localhost:9000/csrf/ +NEXT_PUBLIC_ENVIRONMENT=local +NEXT_PUBLIC_GRAPHQL_URL=http://localhost:9000/graphql/ +NEXT_PUBLIC_GTM_ID= +NEXT_PUBLIC_IDX_URL=http://localhost:9000/idx/ +NEXT_PUBLIC_IS_PROJECT_HEALTH_ENABLED=true +NEXT_PUBLIC_RELEASE_VERSION= +NEXT_PUBLIC_SENTRY_DSN= +NEXT_SERVER_CSRF_URL=http://localhost:9000/csrf/ +NEXT_SERVER_DISABLE_SSR=false +NEXT_SERVER_GITHUB_CLIENT_ID=your-github-client-id +NEXT_SERVER_GITHUB_CLIENT_SECRET=your-github-client-secret +NEXT_SERVER_GRAPHQL_URL=http://localhost:9000/graphql/ diff --git a/frontend/.env.example b/frontend/.env.example index 61379d4ba6..d399643ce0 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,3 +1,5 @@ +NEXTAUTH_SECRET= +NEXTAUTH_URL=http://localhost:3000/ NEXT_PUBLIC_API_URL=http://localhost:8000/ NEXT_PUBLIC_CSRF_URL=http://localhost:8000/csrf/ NEXT_PUBLIC_ENVIRONMENT=local @@ -12,5 +14,3 @@ NEXT_SERVER_DISABLE_SSR=false NEXT_SERVER_GITHUB_CLIENT_ID=your-github-client-id NEXT_SERVER_GITHUB_CLIENT_SECRET=your-github-client-secret NEXT_SERVER_GRAPHQL_URL=http://backend:8000/graphql/ -NEXTAUTH_SECRET= -NEXTAUTH_URL=http://localhost:3000/ diff --git a/frontend/Makefile b/frontend/Makefile index c61cd6aed7..5331db78e4 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -80,11 +80,12 @@ test-frontend-a11y: @docker run --env-file frontend/.env.example --rm nest-test-frontend-a11y pnpm run test:a11y test-frontend-e2e: - @DOCKER_BUILDKIT=1 NEXT_PUBLIC_ENVIRONMENT=local docker build \ - --cache-from nest-test-frontend-e2e \ - -f docker/frontend/Dockerfile.e2e.test frontend \ - -t nest-test-frontend-e2e - @docker run --env-file frontend/.env.example --rm nest-test-frontend-e2e pnpm run test:e2e + @docker container rm -f e2e-nest-db >/dev/null 2>&1 || true + @docker volume rm -f nest-e2e_e2e-db-data >/dev/null 2>&1 || true + @DOCKER_BUILDKIT=1 \ + docker compose --project-name nest-e2e -f docker-compose/e2e/compose.yaml up --build --remove-orphans --abort-on-container-exit db cache backend data-loader + @DOCKER_BUILDKIT=1 NEXT_PUBLIC_ENVIRONMENT=local \ + docker compose --project-name nest-e2e -f docker-compose/e2e/compose.yaml up --build --remove-orphans --abort-on-container-exit db cache backend e2e-tests test-frontend-unit: @DOCKER_BUILDKIT=1 NEXT_PUBLIC_ENVIRONMENT=local docker build \ diff --git a/frontend/__tests__/a11y/components/ChapterMap.a11y.test.tsx b/frontend/__tests__/a11y/components/ChapterMap.a11y.test.tsx index adce4d0b7b..27ef0166a7 100644 --- a/frontend/__tests__/a11y/components/ChapterMap.a11y.test.tsx +++ b/frontend/__tests__/a11y/components/ChapterMap.a11y.test.tsx @@ -14,6 +14,27 @@ const mockMap = { enable: jest.fn(), disable: jest.fn(), }, + dragging: { + enable: jest.fn(), + disable: jest.fn(), + }, + touchZoom: { + enable: jest.fn(), + disable: jest.fn(), + }, + doubleClickZoom: { + enable: jest.fn(), + disable: jest.fn(), + }, + keyboard: { + enable: jest.fn(), + disable: jest.fn(), + }, + getContainer: jest.fn(() => ({ + clientWidth: 800, + clientHeight: 400, + })), + setMinZoom: jest.fn(), } const mockZoomControl = { diff --git a/frontend/__tests__/unit/components/ChapterMap.test.tsx b/frontend/__tests__/unit/components/ChapterMap.test.tsx index 8a05fffce2..727bfc4e96 100644 --- a/frontend/__tests__/unit/components/ChapterMap.test.tsx +++ b/frontend/__tests__/unit/components/ChapterMap.test.tsx @@ -10,10 +10,31 @@ let mockMapInstance: unknown = null const mockMap = { setView: jest.fn().mockReturnThis(), fitBounds: jest.fn().mockReturnThis(), + setMinZoom: jest.fn().mockReturnThis(), + getContainer: jest.fn(() => ({ + clientWidth: 800, + clientHeight: 600, + })), scrollWheelZoom: { enable: jest.fn(), disable: jest.fn(), }, + dragging: { + enable: jest.fn(), + disable: jest.fn(), + }, + touchZoom: { + enable: jest.fn(), + disable: jest.fn(), + }, + doubleClickZoom: { + enable: jest.fn(), + disable: jest.fn(), + }, + keyboard: { + enable: jest.fn(), + disable: jest.fn(), + }, } const mockZoomControl = { diff --git a/frontend/__tests__/unit/components/Pagination.test.tsx b/frontend/__tests__/unit/components/Pagination.test.tsx index 16ee964e30..db26f6287b 100644 --- a/frontend/__tests__/unit/components/Pagination.test.tsx +++ b/frontend/__tests__/unit/components/Pagination.test.tsx @@ -134,6 +134,6 @@ describe('', () => { // Edge-case: very small totalPages (2) it('renders exactly pages [1, 2] when totalPages = 2', () => { renderComponent({ totalPages: 2, currentPage: 2 }) - expect(screen.getAllByRole('button', { name: /^Go to page (1|2)$/ })).toHaveLength(2) + expect(screen.getAllByRole('button', { name: /^Go to page [12]$/ })).toHaveLength(2) }) }) diff --git a/frontend/__tests__/unit/pages/Header.test.tsx b/frontend/__tests__/unit/pages/Header.test.tsx index 9c08ce9898..f60e03cc3b 100644 --- a/frontend/__tests__/unit/pages/Header.test.tsx +++ b/frontend/__tests__/unit/pages/Header.test.tsx @@ -92,30 +92,25 @@ jest.mock('components/UserMenu', () => { }) // Mock constants -jest.mock('utils/constants', () => ({ - desktopViewMinWidth: 768, - headerLinks: [ - { - text: 'Home', - href: '/', - }, - { - text: 'About', - href: '/about', - }, - { - text: 'Services', - submenu: [ - { text: 'Web Development', href: '/services/web' }, - { text: 'Mobile Development', href: '/services/mobile' }, - ], - }, - { - text: 'Contact', - href: '/contact', - }, - ], -})) +jest.mock('utils/constants', () => { + const actual = jest.requireActual('utils/constants') + return { + ...actual, + desktopViewMinWidth: 768, + headerLinks: [ + { text: 'Home', href: '/' }, + { text: 'About', href: '/about' }, + { + text: 'Services', + submenu: [ + { text: 'Web Development', href: '/services/web' }, + { text: 'Mobile Development', href: '/services/mobile' }, + ], + }, + { text: 'Contact', href: '/contact' }, + ], + } +}) // Mock utility function jest.mock('utils/utility', () => ({ @@ -211,8 +206,9 @@ describe('Header Component', () => { const brandTexts = screen.getAllByText('Nest') expect(brandTexts.length).toBe(2) // One in desktop header, one in mobile menu - const userMenu = screen.getByTestId('user-menu') - expect(userMenu).toHaveAttribute('data-github-auth', 'true') + const userMenus = screen.getAllByTestId('user-menu') + expect(userMenus.length).toBeGreaterThanOrEqual(1) + expect(userMenus[0]).toHaveAttribute('data-github-auth', 'true') }) it('renders successfully with GitHub auth disabled', () => { @@ -226,8 +222,9 @@ describe('Header Component', () => { const brandTexts = screen.getAllByText('Nest') expect(brandTexts.length).toBe(2) - const userMenu = screen.getByTestId('user-menu') - expect(userMenu).toHaveAttribute('data-github-auth', 'false') + const userMenus = screen.getAllByTestId('user-menu') + expect(userMenus.length).toBeGreaterThanOrEqual(1) + expect(userMenus[0]).toHaveAttribute('data-github-auth', 'false') }) }) @@ -441,7 +438,9 @@ describe('Header Component', () => { it('renders UserMenu component', () => { renderWithSession(
) - expect(screen.getByTestId('user-menu')).toBeInTheDocument() + const userMenus = screen.getAllByTestId('user-menu') + expect(userMenus.length).toBeGreaterThanOrEqual(1) + expect(userMenus[0]).toBeInTheDocument() }) it('renders ModeToggle component', () => { @@ -581,13 +580,17 @@ describe('Header Component', () => { it('passes isGitHubAuthEnabled prop to UserMenu correctly when true', () => { renderWithSession(
) - expect(screen.getByTestId('user-menu')).toHaveAttribute('data-github-auth', 'true') + const userMenus = screen.getAllByTestId('user-menu') + expect(userMenus.length).toBeGreaterThanOrEqual(1) + expect(userMenus[0]).toHaveAttribute('data-github-auth', 'true') }) it('passes isGitHubAuthEnabled prop to UserMenu correctly when false', () => { renderWithSession(
) - expect(screen.getByTestId('user-menu')).toHaveAttribute('data-github-auth', 'false') + const userMenus = screen.getAllByTestId('user-menu') + expect(userMenus.length).toBeGreaterThanOrEqual(1) + expect(userMenus[0]).toHaveAttribute('data-github-auth', 'false') }) }) diff --git a/frontend/jest.setup.ts b/frontend/jest.setup.ts index 3c700f73e8..dad3bfd0d2 100644 --- a/frontend/jest.setup.ts +++ b/frontend/jest.setup.ts @@ -113,9 +113,9 @@ beforeAll(() => { } globalThis.ResizeObserver = class { - disconnect() {} - observe() {} - unobserve() {} + disconnect() {} // NOSONAR: empty mock implementation for test environment. + observe() {} // NOSONAR: empty mock implementation for test environment. + unobserve() {} // NOSONAR: empty mock implementation for test environment. } }) diff --git a/frontend/next.config.ts b/frontend/next.config.ts index a1a585bd53..ff4ca3667b 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -32,6 +32,7 @@ const nextConfig: NextConfig = { // https://nextjs.org/docs/app/api-reference/config/next-config-js/productionBrowserSourceMaps productionBrowserSourceMaps: true, serverExternalPackages: ['import-in-the-middle', 'require-in-the-middle'], + transpilePackages: ['@react-leaflet/core', 'leaflet', 'react-leaflet', 'react-leaflet-cluster'], ...(isLocal ? {} : { output: 'standalone' }), } diff --git a/frontend/package.json b/frontend/package.json index 8a1ac99c53..02018889d4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "private": true, "scripts": { "build": "next build", - "dev": "next dev --port 3000", + "dev": "next dev --port 3000 --turbo", "format": "prettier --log-level warn --write .", "format:check": "prettier --check .", "graphql-codegen": "graphql-codegen --config graphql-codegen.ts", @@ -18,7 +18,7 @@ "test:unit": "tsc --noEmit && NODE_OPTIONS='--experimental-vm-modules --no-warnings=DEP0040' jest" }, "dependencies": { - "@apollo/client": "^4.0.13", + "@apollo/client": "^4.1.0", "@graphql-typed-document-node/core": "^3.2.0", "@heroui/button": "^2.2.29", "@heroui/modal": "^2.2.26", @@ -29,8 +29,8 @@ "@heroui/theme": "^2.4.25", "@heroui/toast": "^2.0.19", "@heroui/tooltip": "^2.2.26", - "@next/eslint-plugin-next": "^16.1.2", - "@next/third-parties": "^16.1.2", + "@next/eslint-plugin-next": "^16.1.3", + "@next/third-parties": "^16.1.3", "@react-leaflet/core": "^3.0.0", "@sentry/nextjs": "^10.34.0", "@testing-library/user-event": "^14.6.1", @@ -43,7 +43,7 @@ "dayjs": "^1.11.19", "dompurify": "^3.3.1", "eslint-plugin-import": "^2.32.0", - "framer-motion": "^12.26.2", + "framer-motion": "^12.27.0", "graphql": "^16.12.0", "ics": "^3.8.1", "leaflet": "^1.9.4", @@ -52,7 +52,7 @@ "markdown-it": "^14.1.0", "markdown-it-task-lists": "^2.1.1", "millify": "^6.1.0", - "next": "^16.1.2", + "next": "^16.1.3", "next-auth": "^4.24.13", "next-themes": "^0.4.6", "react": "^19.2.3", @@ -85,13 +85,13 @@ "@types/leaflet": "^1.9.21", "@types/leaflet.markercluster": "^1.5.6", "@types/markdown-it": "^14.1.2", - "@types/node": "^25.0.8", + "@types/node": "^25.0.9", "@types/react": "^19.2.8", "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.53.0", "@typescript-eslint/parser": "^8.53.0", "eslint": "^9.39.2", - "eslint-config-next": "^16.1.2", + "eslint-config-next": "^16.1.3", "eslint-config-prettier": "^10.1.8", "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-jest": "^29.12.1", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 78c7e4f59f..4d1d91fdd5 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -13,50 +13,50 @@ importers: .: dependencies: '@apollo/client': - specifier: ^4.0.13 - version: 4.0.13(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.19.0))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rxjs@7.8.2) + specifier: ^4.1.0 + version: 4.1.0(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.19.0))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rxjs@7.8.2) '@graphql-typed-document-node/core': specifier: ^3.2.0 version: 3.2.0(graphql@16.12.0) '@heroui/button': specifier: ^2.2.29 - version: 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/modal': specifier: ^2.2.26 - version: 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react': specifier: ^2.8.7 - version: 2.8.7(@types/react@19.2.8)(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18) + version: 2.8.7(@types/react@19.2.8)(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18) '@heroui/select': specifier: ^2.4.30 - version: 2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/skeleton': specifier: ^2.2.18 - version: 2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/system': specifier: ^2.4.25 - version: 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': specifier: ^2.4.25 version: 2.4.25(tailwindcss@4.1.18) '@heroui/toast': specifier: ^2.0.19 - version: 2.0.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.0.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/tooltip': specifier: ^2.2.26 - version: 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@next/eslint-plugin-next': - specifier: ^16.1.2 - version: 16.1.2 + specifier: ^16.1.3 + version: 16.1.3 '@next/third-parties': - specifier: ^16.1.2 - version: 16.1.2(next@16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + specifier: ^16.1.3 + version: 16.1.3(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) '@react-leaflet/core': specifier: ^3.0.0 version: 3.0.0(leaflet@1.9.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@sentry/nextjs': specifier: ^10.34.0 - version: 10.34.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.18))) + version: 10.34.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.18))) '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) @@ -88,8 +88,8 @@ importers: specifier: ^2.32.0 version: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) framer-motion: - specifier: ^12.26.2 - version: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^12.27.0 + version: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) graphql: specifier: ^16.12.0 version: 16.12.0 @@ -115,11 +115,11 @@ importers: specifier: ^6.1.0 version: 6.1.0 next: - specifier: ^16.1.2 - version: 16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^16.1.3 + version: 16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-auth: specifier: ^4.24.13 - version: 4.24.13(next@16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 4.24.13(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -162,7 +162,7 @@ importers: version: 9.39.2 '@graphql-codegen/cli': specifier: ^6.1.1 - version: 6.1.1(@types/node@25.0.8)(graphql@16.12.0)(typescript@5.9.3) + version: 6.1.1(@types/node@25.0.9)(graphql@16.12.0)(typescript@5.9.3) '@graphql-codegen/near-operation-file-preset': specifier: ^4.0.0 version: 4.0.0(graphql@16.12.0) @@ -209,8 +209,8 @@ importers: specifier: ^14.1.2 version: 14.1.2 '@types/node': - specifier: ^25.0.8 - version: 25.0.8 + specifier: ^25.0.9 + version: 25.0.9 '@types/react': specifier: ^19.2.8 version: 19.2.8 @@ -227,8 +227,8 @@ importers: specifier: ^9.39.2 version: 9.39.2(jiti@2.6.1) eslint-config-next: - specifier: ^16.1.2 - version: 16.1.2(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + specifier: ^16.1.3 + version: 16.1.3(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@9.39.2(jiti@2.6.1)) @@ -237,7 +237,7 @@ importers: version: 1.1.2(eslint-plugin-import@2.32.0) eslint-plugin-jest: specifier: ^29.12.1 - version: 29.12.1(@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.12.1(@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)))(typescript@5.9.3) eslint-plugin-jsx-a11y: specifier: ^6.10.2 version: 6.10.2(eslint@9.39.2(jiti@2.6.1)) @@ -261,7 +261,7 @@ importers: version: 2.0.4 jest: specifier: ^30.2.0 - version: 30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)) + version: 30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)) jest-axe: specifier: ^10.0.0 version: 10.0.0 @@ -285,10 +285,10 @@ importers: version: 4.1.18 ts-jest: specifier: ^29.4.6 - version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)))(typescript@5.9.3) ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3) + version: 10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3) typescript: specifier: ~5.9.3 version: 5.9.3 @@ -314,8 +314,8 @@ packages: '@apm-js-collab/tracing-hooks@0.3.1': resolution: {integrity: sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw==} - '@apollo/client@4.0.13': - resolution: {integrity: sha512-ziUPddxVZ0dg+/l61rFymkPFesENVb3P/a8hKtN1XyawTcydeyRwooM4xBXpakKbt2gxwlm5dvrE1AWEcQlK3g==} + '@apollo/client@4.1.0': + resolution: {integrity: sha512-N/nZXGNBMoHnshNaHXxHZoC42BcIjRqD4XgpmNBPhueoWIbp17VIJe/sGysFNQo1w7DVD78K6gVsNMO87nfgRQ==} peerDependencies: graphql: ^16.0.0 graphql-ws: ^5.5.5 || ^6.0.3 @@ -1929,62 +1929,62 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@16.1.2': - resolution: {integrity: sha512-r6TpLovDTvWtzw11UubUQxEK6IduT8rSAHbGX68yeFpA/1Oq9R4ovi5nqMUMgPN0jr2SpfeyFRbTZg3Inuuv3g==} + '@next/env@16.1.3': + resolution: {integrity: sha512-BLP14oBOvZWXgfdJf9ao+VD8O30uE+x7PaV++QtACLX329WcRSJRO5YJ+Bcvu0Q+c/lei41TjSiFf6pXqnpbQA==} - '@next/eslint-plugin-next@16.1.2': - resolution: {integrity: sha512-jjO5BKDxZEXt2VCAnAG/ldULnpxeXspjCo9AZErV3Lm5HmNj8r2rS+eUMIAAj6mXPAOiPqAMgVPGnkyhPyDx4g==} + '@next/eslint-plugin-next@16.1.3': + resolution: {integrity: sha512-MqBh3ltFAy0AZCRFVdjVjjeV7nEszJDaVIpDAnkQcn8U9ib6OEwkSnuK6xdYxMGPhV/Y4IlY6RbDipPOpLfBqQ==} - '@next/swc-darwin-arm64@16.1.2': - resolution: {integrity: sha512-0N2baysDpTXASTVxTV+DkBnD97bo9PatUj8sHlKA+oR9CyvReaPQchQyhCbH0Jm0mC/Oka5F52intN+lNOhSlA==} + '@next/swc-darwin-arm64@16.1.3': + resolution: {integrity: sha512-CpOD3lmig6VflihVoGxiR/l5Jkjfi4uLaOR4ziriMv0YMDoF6cclI+p5t2nstM8TmaFiY6PCTBgRWB57/+LiBA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.1.2': - resolution: {integrity: sha512-Q0wnSK0lmeC9ps+/w/bDsMSF3iWS45WEwF1bg8dvMH3CmKB2BV4346tVrjWxAkrZq20Ro6Of3R19IgrEJkXKyw==} + '@next/swc-darwin-x64@16.1.3': + resolution: {integrity: sha512-aF4us2JXh0zn3hNxvL1Bx3BOuh8Lcw3p3Xnurlvca/iptrDH1BrpObwkw9WZra7L7/0qB9kjlREq3hN/4x4x+Q==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.1.2': - resolution: {integrity: sha512-4twW+h7ZatGKWq+2pUQ9SDiin6kfZE/mY+D8jOhSZ0NDzKhQfAPReXqwTDWVrNjvLzHzOcDL5kYjADHfXL/b/Q==} + '@next/swc-linux-arm64-gnu@16.1.3': + resolution: {integrity: sha512-8VRkcpcfBtYvhGgXAF7U3MBx6+G1lACM1XCo1JyaUr4KmAkTNP8Dv2wdMq7BI+jqRBw3zQE7c57+lmp7jCFfKA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.1.2': - resolution: {integrity: sha512-Sn6LxPIZcADe5AnqqMCfwBv6vRtDikhtrjwhu+19WM6jHZe31JDRcGuPZAlJrDk6aEbNBPUUAKmySJELkBOesg==} + '@next/swc-linux-arm64-musl@16.1.3': + resolution: {integrity: sha512-UbFx69E2UP7MhzogJRMFvV9KdEn4sLGPicClwgqnLht2TEi204B71HuVfps3ymGAh0c44QRAF+ZmvZZhLLmhNg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@16.1.2': - resolution: {integrity: sha512-nwzesEQBfQIOOnQ7JArzB08w9qwvBQ7nC1i8gb0tiEFH94apzQM3IRpY19MlE8RBHxc9ArG26t1DEg2aaLaqVQ==} + '@next/swc-linux-x64-gnu@16.1.3': + resolution: {integrity: sha512-SzGTfTjR5e9T+sZh5zXqG/oeRQufExxBF6MssXS7HPeZFE98JDhCRZXpSyCfWrWrYrzmnw/RVhlP2AxQm+wkRQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.1.2': - resolution: {integrity: sha512-s60bLf16BDoICQHeKEm0lDgUNMsL1UpQCkRNZk08ZNnRpK0QUV+6TvVHuBcIA7oItzU0m7kVmXe8QjXngYxJVA==} + '@next/swc-linux-x64-musl@16.1.3': + resolution: {integrity: sha512-HlrDpj0v+JBIvQex1mXHq93Mht5qQmfyci+ZNwGClnAQldSfxI6h0Vupte1dSR4ueNv4q7qp5kTnmLOBIQnGow==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@16.1.2': - resolution: {integrity: sha512-Sq8k4SZd8Y8EokKdz304TvMO9HoiwGzo0CTacaiN1bBtbJSQ1BIwKzNFeFdxOe93SHn1YGnKXG6Mq3N+tVooyQ==} + '@next/swc-win32-arm64-msvc@16.1.3': + resolution: {integrity: sha512-3gFCp83/LSduZMSIa+lBREP7+5e7FxpdBoc9QrCdmp+dapmTK9I+SLpY60Z39GDmTXSZA4huGg9WwmYbr6+WRw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.1.2': - resolution: {integrity: sha512-KQDBwspSaNX5/wwt6p7ed5oINJWIxcgpuqJdDNubAyq7dD+ZM76NuEjg8yUxNOl5R4NNgbMfqE/RyNrsbYmOKg==} + '@next/swc-win32-x64-msvc@16.1.3': + resolution: {integrity: sha512-1SZVfFT8zmMB+Oblrh5OKDvUo5mYQOkX2We6VGzpg7JUVZlqe4DYOFGKYZKTweSx1gbMixyO1jnFT4thU+nNHQ==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@next/third-parties@16.1.2': - resolution: {integrity: sha512-l7caHpg4DQub6R4pgAFpREL+BuCnELweT2ch+vfFdMj312j3Yu1yc0lbZTxqwoaj24dHb5tVGiSL08i3rXkN8w==} + '@next/third-parties@16.1.3': + resolution: {integrity: sha512-jfsjVs/w2MGSF/+2Miy9iLw5aPShzcfNEAhRQeMB7wCThdUXnA/f9UjXLlAQDqbRhQ0qKV2vuTyWaiFwRAp+kQ==} peerDependencies: next: ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0-beta.0 react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 @@ -2185,8 +2185,8 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.3.0 <1.10.0' - '@opentelemetry/semantic-conventions@1.38.0': - resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} + '@opentelemetry/semantic-conventions@1.39.0': + resolution: {integrity: sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==} engines: {node: '>=14'} '@opentelemetry/sql-common@0.41.2': @@ -3229,8 +3229,8 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@theguild/federation-composition@0.21.2': - resolution: {integrity: sha512-vkaJrMaG5TXtEzdrbfTZ5IhOot/Ct2aZHEgG4fYmzZa037DpLaic6W1l1NIlFZ7c/gHZS3Wz4Wt3ZTvsDgOAOQ==} + '@theguild/federation-composition@0.21.3': + resolution: {integrity: sha512-+LlHTa4UbRpZBog3ggAxjYIFvdfH3UMvvBUptur19TMWkqU4+n3GmN+mDjejU+dyBXIG27c25RsiQP1HyvM99g==} engines: {node: '>=18'} peerDependencies: graphql: ^16.0.0 @@ -3325,8 +3325,8 @@ packages: '@types/mysql@2.15.27': resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} - '@types/node@25.0.8': - resolution: {integrity: sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg==} + '@types/node@25.0.9': + resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==} '@types/pg-pool@2.0.6': resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} @@ -3883,8 +3883,8 @@ packages: bare-url@2.3.2: resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} - baseline-browser-mapping@2.9.14: - resolution: {integrity: sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==} + baseline-browser-mapping@2.9.15: + resolution: {integrity: sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg==} hasBin: true basic-ftp@5.1.0: @@ -3958,8 +3958,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001764: - resolution: {integrity: sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==} + caniuse-lite@1.0.30001765: + resolution: {integrity: sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==} capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} @@ -4486,8 +4486,8 @@ packages: engines: {node: '>=6.0'} hasBin: true - eslint-config-next@16.1.2: - resolution: {integrity: sha512-y97rpFfUsaXdXlQc2FMl/yqRc5yfVVKtKRcv+7LeyBrKh83INFegJuZBE28dc9Chp4iKXwmjaW4sHHx/mgyDyA==} + eslint-config-next@16.1.3: + resolution: {integrity: sha512-q2Z87VSsoJcv+vgR+Dm8NPRf+rErXcRktuBR5y3umo/j5zLjIWH7rqBCh3X804gUGKbOrqbgsLUkqDE35C93Gw==} peerDependencies: eslint: '>=9.0.0' typescript: '>=3.3.1' @@ -4804,8 +4804,8 @@ packages: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} - framer-motion@12.26.2: - resolution: {integrity: sha512-lflOQEdjquUi9sCg5Y1LrsZDlsjrHw7m0T9Yedvnk7Bnhqfkc89/Uha10J3CFhkL+TCZVCRw9eUGyM/lyYhXQA==} + framer-motion@12.27.0: + resolution: {integrity: sha512-gJtqOKEDJH/jrn0PpsWp64gdOjBvGX8hY6TWstxjDot/85daIEtJHl1UsiwHSXiYmJF2QXUoXP6/3gGw5xY2YA==} peerDependencies: '@emotion/is-prop-valid': '*' react: ^18.0.0 || ^19.0.0 @@ -5945,8 +5945,8 @@ packages: module-details-from-path@1.0.4: resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - motion-dom@12.26.2: - resolution: {integrity: sha512-KLMT1BroY8oKNeliA3JMNJ+nbCIsTKg6hJpDb4jtRAJ7nCKnnpg/LTq/NGqG90Limitz3kdAnAVXecdFVGlWTw==} + motion-dom@12.27.0: + resolution: {integrity: sha512-oDjl0WoAsWIWKl3GCDxmh7GITrNjmLX+w5+jwk4+pzLu3VnFvsOv2E6+xCXeH72O65xlXsr84/otiOYQKW/nQA==} motion-utils@12.24.10: resolution: {integrity: sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww==} @@ -6012,8 +6012,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.1.2: - resolution: {integrity: sha512-SVSWX7wjUUDrIDVqhl4xm/jiOrvYGMG7NzVE/dGzzgs7r3dFGm4V19ia0xn3GDNtHCKM7C9h+5BoimnJBhmt9A==} + next@16.1.3: + resolution: {integrity: sha512-gthG3TRD+E3/mA0uDQb9lqBmx1zVosq5kIwxNN6+MRNd085GzD+9VXMPUs+GGZCbZ+GDZdODUq4Pm7CTXK6ipw==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -7053,8 +7053,8 @@ packages: uglify-js: optional: true - terser@5.45.0: - resolution: {integrity: sha512-hQ9c+JZEnMug8eqzuU48sCeq95f00lLDAaJ5gWhRkFXsfy3+SUkZXiF/Z66ZO6EomSmgqXnkhVrWXKaQ8K41Ug==} + terser@5.46.0: + resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} engines: {node: '>=10'} hasBin: true @@ -7619,7 +7619,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@apollo/client@4.0.13(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.19.0))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rxjs@7.8.2)': + '@apollo/client@4.1.0(graphql-ws@6.0.6(graphql@16.12.0)(ws@8.19.0))(graphql@16.12.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rxjs@7.8.2)': dependencies: '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) '@wry/caches': 1.0.1 @@ -7997,7 +7997,7 @@ snapshots: graphql: 16.12.0 tslib: 2.6.3 - '@graphql-codegen/cli@6.1.1(@types/node@25.0.8)(graphql@16.12.0)(typescript@5.9.3)': + '@graphql-codegen/cli@6.1.1(@types/node@25.0.9)(graphql@16.12.0)(typescript@5.9.3)': dependencies: '@babel/generator': 7.28.6 '@babel/template': 7.28.6 @@ -8008,20 +8008,20 @@ snapshots: '@graphql-tools/apollo-engine-loader': 8.0.28(graphql@16.12.0) '@graphql-tools/code-file-loader': 8.1.28(graphql@16.12.0) '@graphql-tools/git-loader': 8.0.32(graphql@16.12.0) - '@graphql-tools/github-loader': 9.0.6(@types/node@25.0.8)(graphql@16.12.0) + '@graphql-tools/github-loader': 9.0.6(@types/node@25.0.9)(graphql@16.12.0) '@graphql-tools/graphql-file-loader': 8.1.9(graphql@16.12.0) '@graphql-tools/json-file-loader': 8.0.26(graphql@16.12.0) '@graphql-tools/load': 8.1.8(graphql@16.12.0) - '@graphql-tools/url-loader': 9.0.6(@types/node@25.0.8)(graphql@16.12.0) + '@graphql-tools/url-loader': 9.0.6(@types/node@25.0.9)(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) - '@inquirer/prompts': 7.10.1(@types/node@25.0.8) + '@inquirer/prompts': 7.10.1(@types/node@25.0.9) '@whatwg-node/fetch': 0.10.13 chalk: 4.1.2 cosmiconfig: 9.0.0(typescript@5.9.3) debounce: 2.2.0 detect-indent: 6.1.0 graphql: 16.12.0 - graphql-config: 5.1.5(@types/node@25.0.8)(graphql@16.12.0)(typescript@5.9.3) + graphql-config: 5.1.5(@types/node@25.0.9)(graphql@16.12.0)(typescript@5.9.3) is-glob: 4.0.3 jiti: 2.6.1 json-to-pretty-yaml: 1.2.2 @@ -8285,7 +8285,7 @@ snapshots: - uWebSockets.js - utf-8-validate - '@graphql-tools/executor-http@1.3.3(@types/node@25.0.8)(graphql@16.12.0)': + '@graphql-tools/executor-http@1.3.3(@types/node@25.0.9)(graphql@16.12.0)': dependencies: '@graphql-hive/signal': 1.0.0 '@graphql-tools/executor-common': 0.0.4(graphql@16.12.0) @@ -8295,12 +8295,12 @@ snapshots: '@whatwg-node/fetch': 0.10.13 '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 - meros: 1.3.2(@types/node@25.0.8) + meros: 1.3.2(@types/node@25.0.9) tslib: 2.8.1 transitivePeerDependencies: - '@types/node' - '@graphql-tools/executor-http@3.1.0(@types/node@25.0.8)(graphql@16.12.0)': + '@graphql-tools/executor-http@3.1.0(@types/node@25.0.9)(graphql@16.12.0)': dependencies: '@graphql-hive/signal': 2.0.0 '@graphql-tools/executor-common': 1.0.6(graphql@16.12.0) @@ -8310,7 +8310,7 @@ snapshots: '@whatwg-node/fetch': 0.10.13 '@whatwg-node/promise-helpers': 1.3.2 graphql: 16.12.0 - meros: 1.3.2(@types/node@25.0.8) + meros: 1.3.2(@types/node@25.0.9) tslib: 2.8.1 transitivePeerDependencies: - '@types/node' @@ -8349,9 +8349,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@graphql-tools/github-loader@9.0.6(@types/node@25.0.8)(graphql@16.12.0)': + '@graphql-tools/github-loader@9.0.6(@types/node@25.0.9)(graphql@16.12.0)': dependencies: - '@graphql-tools/executor-http': 3.1.0(@types/node@25.0.8)(graphql@16.12.0) + '@graphql-tools/executor-http': 3.1.0(@types/node@25.0.9)(graphql@16.12.0) '@graphql-tools/graphql-tag-pluck': 8.3.27(graphql@16.12.0) '@graphql-tools/utils': 11.0.0(graphql@16.12.0) '@whatwg-node/fetch': 0.10.13 @@ -8390,7 +8390,7 @@ snapshots: '@graphql-tools/import@7.1.9(graphql@16.12.0)': dependencies: '@graphql-tools/utils': 11.0.0(graphql@16.12.0) - '@theguild/federation-composition': 0.21.2(graphql@16.12.0) + '@theguild/federation-composition': 0.21.3(graphql@16.12.0) graphql: 16.12.0 resolve-from: 5.0.0 tslib: 2.8.1 @@ -8440,10 +8440,10 @@ snapshots: graphql: 16.12.0 tslib: 2.8.1 - '@graphql-tools/url-loader@8.0.33(@types/node@25.0.8)(graphql@16.12.0)': + '@graphql-tools/url-loader@8.0.33(@types/node@25.0.9)(graphql@16.12.0)': dependencies: '@graphql-tools/executor-graphql-ws': 2.0.7(graphql@16.12.0) - '@graphql-tools/executor-http': 1.3.3(@types/node@25.0.8)(graphql@16.12.0) + '@graphql-tools/executor-http': 1.3.3(@types/node@25.0.9)(graphql@16.12.0) '@graphql-tools/executor-legacy-ws': 1.1.25(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@graphql-tools/wrap': 10.1.4(graphql@16.12.0) @@ -8463,10 +8463,10 @@ snapshots: - uWebSockets.js - utf-8-validate - '@graphql-tools/url-loader@9.0.6(@types/node@25.0.8)(graphql@16.12.0)': + '@graphql-tools/url-loader@9.0.6(@types/node@25.0.9)(graphql@16.12.0)': dependencies: '@graphql-tools/executor-graphql-ws': 3.1.4(graphql@16.12.0) - '@graphql-tools/executor-http': 3.1.0(@types/node@25.0.8)(graphql@16.12.0) + '@graphql-tools/executor-http': 3.1.0(@types/node@25.0.9)(graphql@16.12.0) '@graphql-tools/executor-legacy-ws': 1.1.25(graphql@16.12.0) '@graphql-tools/utils': 11.0.0(graphql@16.12.0) '@graphql-tools/wrap': 11.1.4(graphql@16.12.0) @@ -8524,16 +8524,16 @@ snapshots: dependencies: graphql: 16.12.0 - '@heroui/accordion@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/accordion@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/divider': 2.2.21(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/dom-animation': 2.1.10(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/dom-animation': 2.1.10(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-aria-accordion': 2.2.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8541,17 +8541,17 @@ snapshots: '@react-stately/tree': 3.9.4(react@19.2.3) '@react-types/accordion': 3.0.0-alpha.26(react@19.2.3) '@react-types/shared': 3.32.1(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/alert@2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/alert@2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@react-stately/utils': 3.11.0(react@19.2.3) react: 19.2.3 @@ -8559,9 +8559,9 @@ snapshots: transitivePeerDependencies: - framer-motion - '@heroui/aria-utils@2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/aria-utils@2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/utils': 3.32.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-stately/collections': 3.12.8(react@19.2.3) '@react-types/overlays': 3.9.2(react@19.2.3) @@ -8572,19 +8572,19 @@ snapshots: - '@heroui/theme' - framer-motion - '@heroui/autocomplete@2.3.31(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(@types/react@19.2.8)(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/autocomplete@2.3.31(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(@types/react@19.2.8)(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/input': 2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/listbox': 2.3.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/popover': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/input': 2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/listbox': 2.3.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/popover': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) - '@heroui/scroll-shadow': 2.3.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/scroll-shadow': 2.3.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.3) '@react-aria/combobox': 3.14.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8592,17 +8592,17 @@ snapshots: '@react-stately/combobox': 3.12.1(react@19.2.3) '@react-types/combobox': 3.13.10(react@19.2.3) '@react-types/shared': 3.32.1(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - '@types/react' - '@heroui/avatar@2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/avatar@2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-image': 2.1.13(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8610,21 +8610,21 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/badge@2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/badge@2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/breadcrumbs@2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/breadcrumbs@2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@react-aria/breadcrumbs': 3.5.30(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8632,31 +8632,31 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/button@2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/button@2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) - '@heroui/ripple': 2.2.21(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/ripple': 2.2.21(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/spinner': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/spinner': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.21(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/interactions': 3.26.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-types/shared': 3.32.1(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/calendar@2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/calendar@2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/dom-animation': 2.1.10(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/dom-animation': 2.1.10(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.21(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@internationalized/date': 3.10.1 @@ -8670,32 +8670,32 @@ snapshots: '@react-types/button': 3.14.1(react@19.2.3) '@react-types/calendar': 3.8.1(react@19.2.3) '@react-types/shared': 3.32.1(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) scroll-into-view-if-needed: 3.0.10 - '@heroui/card@2.2.27(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/card@2.2.27(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) - '@heroui/ripple': 2.2.21(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/ripple': 2.2.21(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.21(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/interactions': 3.26.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-types/shared': 3.32.1(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/checkbox@2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/checkbox@2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-callback-ref': 2.1.8(react@19.2.3) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.3) @@ -8709,12 +8709,12 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/chip@2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/chip@2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/interactions': 3.26.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8730,12 +8730,12 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/date-input@2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/date-input@2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@internationalized/date': 3.10.1 '@react-aria/datepicker': 3.15.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8746,18 +8746,18 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/date-picker@2.3.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/date-picker@2.3.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/calendar': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/date-input': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/popover': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/calendar': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/date-input': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/popover': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@internationalized/date': 3.10.1 '@react-aria/datepicker': 3.15.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8766,7 +8766,7 @@ snapshots: '@react-stately/utils': 3.11.0(react@19.2.3) '@react-types/datepicker': 3.13.3(react@19.2.3) '@react-types/shared': 3.32.1(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -8779,44 +8779,44 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/dom-animation@2.1.10(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': + '@heroui/dom-animation@2.1.10(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))': dependencies: - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/drawer@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/drawer@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/modal': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/modal': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - framer-motion - '@heroui/dropdown@2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/dropdown@2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/menu': 2.2.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/popover': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/menu': 2.2.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/popover': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/menu': 3.19.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-stately/menu': 3.9.9(react@19.2.3) '@react-types/menu': 3.10.5(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/form@2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/form@2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@react-stately/form': 3.2.2(react@19.2.3) '@react-types/form': 3.7.16(react@19.2.3) @@ -8824,32 +8824,32 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/framer-utils@2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/framer-utils@2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/use-measure': 2.1.8(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - '@heroui/theme' - '@heroui/image@2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/image@2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-image': 2.1.13(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/input-otp@2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/input-otp@2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-form-reset': 2.0.1(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8861,13 +8861,13 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/input@2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/input@2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8891,12 +8891,12 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/link@2.2.25(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/link@2.2.25(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-aria-link': 2.2.22(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8904,13 +8904,13 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/listbox@2.3.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/listbox@2.3.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/divider': 2.2.21(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-is-mobile': 2.2.12(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8924,13 +8924,13 @@ snapshots: transitivePeerDependencies: - framer-motion - '@heroui/menu@2.2.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/menu@2.2.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/divider': 2.2.21(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-is-mobile': 2.2.12(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8944,14 +8944,14 @@ snapshots: transitivePeerDependencies: - framer-motion - '@heroui/modal@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/modal@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/dom-animation': 2.1.10(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/dom-animation': 2.1.10(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.21(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/use-aria-modal-overlay': 2.2.20(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8962,17 +8962,17 @@ snapshots: '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/overlays': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-stately/overlays': 3.6.21(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/navbar@2.2.27(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/navbar@2.2.27(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/dom-animation': 2.1.10(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/dom-animation': 2.1.10(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-resize': 2.1.8(react@19.2.3) '@heroui/use-scroll-position': 2.1.8(react@19.2.3) @@ -8982,18 +8982,18 @@ snapshots: '@react-aria/overlays': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-stately/toggle': 3.9.3(react@19.2.3) '@react-stately/utils': 3.11.0(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/number-input@2.0.20(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/number-input@2.0.20(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -9009,12 +9009,12 @@ snapshots: transitivePeerDependencies: - framer-motion - '@heroui/pagination@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/pagination@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-intersection-observer': 2.2.14(react@19.2.3) '@heroui/use-pagination': 2.2.19(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -9026,15 +9026,15 @@ snapshots: react-dom: 19.2.3(react@19.2.3) scroll-into-view-if-needed: 3.0.10 - '@heroui/popover@2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/popover@2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/dom-animation': 2.1.10(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/dom-animation': 2.1.10(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.21(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/use-aria-overlay': 2.0.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -9044,15 +9044,15 @@ snapshots: '@react-aria/overlays': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-stately/overlays': 3.6.21(react@19.2.3) '@react-types/overlays': 3.9.2(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/progress@2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/progress@2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-is-mounted': 2.1.8(react@19.2.3) '@react-aria/progress': 3.4.28(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -9060,12 +9060,12 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/radio@2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/radio@2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/interactions': 3.26.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -9087,97 +9087,97 @@ snapshots: '@heroui/shared-utils': 2.1.12 react: 19.2.3 - '@heroui/react@2.8.7(@types/react@19.2.8)(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)': - dependencies: - '@heroui/accordion': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/alert': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/autocomplete': 2.3.31(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(@types/react@19.2.8)(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/avatar': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/badge': 2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/breadcrumbs': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/calendar': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/card': 2.2.27(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/checkbox': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/chip': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/react@2.8.7(@types/react@19.2.8)(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.1.18)': + dependencies: + '@heroui/accordion': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/alert': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/autocomplete': 2.3.31(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(@types/react@19.2.8)(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/avatar': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/badge': 2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/breadcrumbs': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/calendar': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/card': 2.2.27(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/checkbox': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/chip': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/code': 2.2.22(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/date-input': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/date-picker': 2.3.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/date-input': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/date-picker': 2.3.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/divider': 2.2.21(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/drawer': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/dropdown': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/image': 2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/input': 2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/input-otp': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/drawer': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/dropdown': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/image': 2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/input': 2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(@types/react@19.2.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/input-otp': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/kbd': 2.2.23(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/link': 2.2.25(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/listbox': 2.3.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/menu': 2.2.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/modal': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/navbar': 2.2.27(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/number-input': 2.0.20(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/pagination': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/popover': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/progress': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/radio': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/ripple': 2.2.21(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/scroll-shadow': 2.3.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/select': 2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/skeleton': 2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/slider': 2.4.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/snippet': 2.2.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/link': 2.2.25(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/listbox': 2.3.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/menu': 2.2.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/modal': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/navbar': 2.2.27(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/number-input': 2.0.20(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/pagination': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/popover': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/progress': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/radio': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/ripple': 2.2.21(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/scroll-shadow': 2.3.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/select': 2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/skeleton': 2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/slider': 2.4.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/snippet': 2.2.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/spacer': 2.2.22(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/spinner': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/switch': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/table': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/tabs': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/spinner': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/switch': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/table': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/tabs': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) - '@heroui/toast': 2.0.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/tooltip': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/user': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/toast': 2.0.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/tooltip': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/user': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/visually-hidden': 3.8.29(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - '@types/react' - tailwindcss - '@heroui/ripple@2.2.21(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/ripple@2.2.21(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/dom-animation': 2.1.10(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@heroui/dom-animation': 2.1.10(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/scroll-shadow@2.3.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/scroll-shadow@2.3.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-data-scroll-overflow': 2.2.13(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/select@2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/select@2.4.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/listbox': 2.3.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/popover': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/form': 2.1.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/listbox': 2.3.28(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/popover': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) - '@heroui/scroll-shadow': 2.3.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/scroll-shadow': 2.3.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/spinner': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/spinner': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-aria-button': 2.2.21(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/use-aria-multiselect': 2.4.20(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -9189,7 +9189,7 @@ snapshots: '@react-aria/overlays': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/visually-hidden': 3.8.29(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-types/shared': 3.32.1(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -9199,21 +9199,21 @@ snapshots: '@heroui/shared-utils@2.1.12': {} - '@heroui/skeleton@2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/skeleton@2.2.18(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/slider@2.4.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/slider@2.4.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) - '@heroui/tooltip': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/tooltip': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/i18n': 3.12.14(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/interactions': 3.26.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -9225,18 +9225,18 @@ snapshots: transitivePeerDependencies: - framer-motion - '@heroui/snippet@2.2.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/snippet@2.2.30(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/button': 2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) - '@heroui/tooltip': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/tooltip': 2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/use-clipboard': 2.1.9(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -9249,10 +9249,10 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/spinner@2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/spinner@2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/system-rsc': 2.3.21(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) react: 19.2.3 @@ -9260,11 +9260,11 @@ snapshots: transitivePeerDependencies: - framer-motion - '@heroui/switch@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/switch@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -9281,26 +9281,26 @@ snapshots: '@react-types/shared': 3.32.1(react@19.2.3) react: 19.2.3 - '@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/system-rsc': 2.3.21(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react@19.2.3) '@react-aria/i18n': 3.12.14(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/overlays': 3.31.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/utils': 3.32.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - '@heroui/theme' - '@heroui/table@2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/table@2.2.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/checkbox': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/checkbox': 2.3.29(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/interactions': 3.26.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -9314,12 +9314,12 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/tabs@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/tabs@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-is-mounted': 2.1.8(react@19.2.3) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -9327,7 +9327,7 @@ snapshots: '@react-aria/tabs': 3.10.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-stately/tabs': 3.8.7(react@19.2.3) '@react-types/shared': 3.32.1(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) scroll-into-view-if-needed: 3.0.10 @@ -9343,30 +9343,30 @@ snapshots: tailwind-variants: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.1.18) tailwindcss: 4.1.18 - '@heroui/toast@2.0.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/toast@2.0.19(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-icons': 2.1.10(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/spinner': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/spinner': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-is-mobile': 2.2.12(react@19.2.3) '@react-aria/interactions': 3.26.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-aria/toast': 3.0.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-stately/toast': 3.1.2(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@heroui/tooltip@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/tooltip@2.2.26(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@heroui/dom-animation': 2.1.10(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) - '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/aria-utils': 2.2.26(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/dom-animation': 2.1.10(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + '@heroui/framer-utils': 2.1.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@heroui/use-aria-overlay': 2.0.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/use-safe-layout-effect': 2.1.8(react@19.2.3) @@ -9375,7 +9375,7 @@ snapshots: '@react-stately/tooltip': 3.5.9(react@19.2.3) '@react-types/overlays': 3.9.2(react@19.2.3) '@react-types/tooltip': 3.5.0(react@19.2.3) - framer-motion: 12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -9530,12 +9530,12 @@ snapshots: dependencies: react: 19.2.3 - '@heroui/user@2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@heroui/user@2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@heroui/avatar': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/avatar': 2.2.24(@heroui/system@2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(@heroui/theme@2.4.25(tailwindcss@4.1.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/react-utils': 2.1.14(react@19.2.3) '@heroui/shared-utils': 2.1.12 - '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@heroui/system': 2.4.25(@heroui/theme@2.4.25(tailwindcss@4.1.18))(framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@heroui/theme': 2.4.25(tailwindcss@4.1.18) '@react-aria/focus': 3.21.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 @@ -9651,128 +9651,128 @@ snapshots: '@inquirer/ansi@1.0.2': {} - '@inquirer/checkbox@4.3.2(@types/node@25.0.8)': + '@inquirer/checkbox@4.3.2(@types/node@25.0.9)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.0.8) + '@inquirer/core': 10.3.2(@types/node@25.0.9) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/type': 3.0.10(@types/node@25.0.9) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/confirm@5.1.21(@types/node@25.0.8)': + '@inquirer/confirm@5.1.21(@types/node@25.0.9)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.8) - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/core': 10.3.2(@types/node@25.0.9) + '@inquirer/type': 3.0.10(@types/node@25.0.9) optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/core@10.3.2(@types/node@25.0.8)': + '@inquirer/core@10.3.2(@types/node@25.0.9)': dependencies: '@inquirer/ansi': 1.0.2 '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/type': 3.0.10(@types/node@25.0.9) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/editor@4.2.23(@types/node@25.0.8)': + '@inquirer/editor@4.2.23(@types/node@25.0.9)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.8) - '@inquirer/external-editor': 1.0.3(@types/node@25.0.8) - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/core': 10.3.2(@types/node@25.0.9) + '@inquirer/external-editor': 1.0.3(@types/node@25.0.9) + '@inquirer/type': 3.0.10(@types/node@25.0.9) optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/expand@4.0.23(@types/node@25.0.8)': + '@inquirer/expand@4.0.23(@types/node@25.0.9)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.8) - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/core': 10.3.2(@types/node@25.0.9) + '@inquirer/type': 3.0.10(@types/node@25.0.9) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/external-editor@1.0.3(@types/node@25.0.8)': + '@inquirer/external-editor@1.0.3(@types/node@25.0.9)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 '@inquirer/figures@1.0.15': {} - '@inquirer/input@4.3.1(@types/node@25.0.8)': + '@inquirer/input@4.3.1(@types/node@25.0.9)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.8) - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/core': 10.3.2(@types/node@25.0.9) + '@inquirer/type': 3.0.10(@types/node@25.0.9) optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/number@3.0.23(@types/node@25.0.8)': + '@inquirer/number@3.0.23(@types/node@25.0.9)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.8) - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/core': 10.3.2(@types/node@25.0.9) + '@inquirer/type': 3.0.10(@types/node@25.0.9) optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/password@4.0.23(@types/node@25.0.8)': + '@inquirer/password@4.0.23(@types/node@25.0.9)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.0.8) - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/core': 10.3.2(@types/node@25.0.9) + '@inquirer/type': 3.0.10(@types/node@25.0.9) optionalDependencies: - '@types/node': 25.0.8 - - '@inquirer/prompts@7.10.1(@types/node@25.0.8)': - dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@25.0.8) - '@inquirer/confirm': 5.1.21(@types/node@25.0.8) - '@inquirer/editor': 4.2.23(@types/node@25.0.8) - '@inquirer/expand': 4.0.23(@types/node@25.0.8) - '@inquirer/input': 4.3.1(@types/node@25.0.8) - '@inquirer/number': 3.0.23(@types/node@25.0.8) - '@inquirer/password': 4.0.23(@types/node@25.0.8) - '@inquirer/rawlist': 4.1.11(@types/node@25.0.8) - '@inquirer/search': 3.2.2(@types/node@25.0.8) - '@inquirer/select': 4.4.2(@types/node@25.0.8) + '@types/node': 25.0.9 + + '@inquirer/prompts@7.10.1(@types/node@25.0.9)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@25.0.9) + '@inquirer/confirm': 5.1.21(@types/node@25.0.9) + '@inquirer/editor': 4.2.23(@types/node@25.0.9) + '@inquirer/expand': 4.0.23(@types/node@25.0.9) + '@inquirer/input': 4.3.1(@types/node@25.0.9) + '@inquirer/number': 3.0.23(@types/node@25.0.9) + '@inquirer/password': 4.0.23(@types/node@25.0.9) + '@inquirer/rawlist': 4.1.11(@types/node@25.0.9) + '@inquirer/search': 3.2.2(@types/node@25.0.9) + '@inquirer/select': 4.4.2(@types/node@25.0.9) optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/rawlist@4.1.11(@types/node@25.0.8)': + '@inquirer/rawlist@4.1.11(@types/node@25.0.9)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.8) - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/core': 10.3.2(@types/node@25.0.9) + '@inquirer/type': 3.0.10(@types/node@25.0.9) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/search@3.2.2(@types/node@25.0.8)': + '@inquirer/search@3.2.2(@types/node@25.0.9)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.8) + '@inquirer/core': 10.3.2(@types/node@25.0.9) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/type': 3.0.10(@types/node@25.0.9) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/select@4.4.2(@types/node@25.0.8)': + '@inquirer/select@4.4.2(@types/node@25.0.9)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.0.8) + '@inquirer/core': 10.3.2(@types/node@25.0.9) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.0.8) + '@inquirer/type': 3.0.10(@types/node@25.0.9) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@inquirer/type@3.0.10(@types/node@25.0.8)': + '@inquirer/type@3.0.10(@types/node@25.0.9)': optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 '@internationalized/date@3.10.1': dependencies: @@ -9819,13 +9819,13 @@ snapshots: '@jest/console@30.2.0': dependencies: '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 chalk: 4.1.2 jest-message-util: 30.2.0 jest-util: 30.2.0 slash: 3.0.0 - '@jest/core@30.2.0(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3))': + '@jest/core@30.2.0(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3))': dependencies: '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 @@ -9833,14 +9833,14 @@ snapshots: '@jest/test-result': 30.2.0 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 ansi-escapes: 4.3.2 chalk: 4.1.2 ci-info: 4.3.1 exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)) jest-haste-map: 30.2.0 jest-message-util: 30.2.0 jest-regex-util: 30.0.1 @@ -9873,7 +9873,7 @@ snapshots: '@jest/fake-timers': 30.2.0 '@jest/types': 30.2.0 '@types/jsdom': 21.1.7 - '@types/node': 25.0.8 + '@types/node': 25.0.9 jest-mock: 30.2.0 jest-util: 30.2.0 jsdom: 26.1.0 @@ -9882,7 +9882,7 @@ snapshots: dependencies: '@jest/fake-timers': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 jest-mock: 30.2.0 '@jest/expect-utils@30.2.0': @@ -9900,7 +9900,7 @@ snapshots: dependencies: '@jest/types': 30.2.0 '@sinonjs/fake-timers': 13.0.5 - '@types/node': 25.0.8 + '@types/node': 25.0.9 jest-message-util: 30.2.0 jest-mock: 30.2.0 jest-util: 30.2.0 @@ -9918,7 +9918,7 @@ snapshots: '@jest/pattern@30.0.1': dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 jest-regex-util: 30.0.1 '@jest/reporters@30.2.0': @@ -9929,7 +9929,7 @@ snapshots: '@jest/transform': 30.2.0 '@jest/types': 30.2.0 '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 25.0.8 + '@types/node': 25.0.9 chalk: 4.1.2 collect-v8-coverage: 1.0.3 exit-x: 0.2.2 @@ -10010,7 +10010,7 @@ snapshots: '@jest/schemas': 30.0.5 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 25.0.8 + '@types/node': 25.0.9 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -10092,39 +10092,39 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@16.1.2': {} + '@next/env@16.1.3': {} - '@next/eslint-plugin-next@16.1.2': + '@next/eslint-plugin-next@16.1.3': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.1.2': + '@next/swc-darwin-arm64@16.1.3': optional: true - '@next/swc-darwin-x64@16.1.2': + '@next/swc-darwin-x64@16.1.3': optional: true - '@next/swc-linux-arm64-gnu@16.1.2': + '@next/swc-linux-arm64-gnu@16.1.3': optional: true - '@next/swc-linux-arm64-musl@16.1.2': + '@next/swc-linux-arm64-musl@16.1.3': optional: true - '@next/swc-linux-x64-gnu@16.1.2': + '@next/swc-linux-x64-gnu@16.1.3': optional: true - '@next/swc-linux-x64-musl@16.1.2': + '@next/swc-linux-x64-musl@16.1.3': optional: true - '@next/swc-win32-arm64-msvc@16.1.2': + '@next/swc-win32-arm64-msvc@16.1.3': optional: true - '@next/swc-win32-x64-msvc@16.1.2': + '@next/swc-win32-x64-msvc@16.1.3': optional: true - '@next/third-parties@16.1.2(next@16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + '@next/third-parties@16.1.3(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': dependencies: - next: 16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 third-party-capital: 1.0.20 @@ -10155,12 +10155,12 @@ snapshots: '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/instrumentation-amqplib@0.55.0(@opentelemetry/api@1.9.0)': dependencies: @@ -10175,7 +10175,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 '@types/connect': 3.4.38 transitivePeerDependencies: - supports-color @@ -10192,7 +10192,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color @@ -10223,7 +10223,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color @@ -10232,7 +10232,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 forwarded-parse: 2.1.2 transitivePeerDependencies: - supports-color @@ -10249,7 +10249,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color @@ -10257,7 +10257,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color @@ -10266,7 +10266,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color @@ -10296,7 +10296,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -10314,7 +10314,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.0) '@types/pg': 8.15.6 '@types/pg-pool': 2.0.6 @@ -10326,7 +10326,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.38.2 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color @@ -10343,7 +10343,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 transitivePeerDependencies: - supports-color @@ -10362,16 +10362,16 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 '@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 - '@opentelemetry/semantic-conventions@1.38.0': {} + '@opentelemetry/semantic-conventions@1.39.0': {} '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.0)': dependencies: @@ -11395,20 +11395,20 @@ snapshots: '@sentry/utils': 7.120.4 localforage: 1.10.0 - '@sentry/nextjs@10.34.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.18)))': + '@sentry/nextjs@10.34.0(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.18)))': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 '@rollup/plugin-commonjs': 28.0.1(rollup@4.55.1) '@sentry-internal/browser-utils': 10.34.0 '@sentry/bundler-plugin-core': 4.6.2 '@sentry/core': 10.34.0 '@sentry/node': 10.34.0 - '@sentry/opentelemetry': 10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0) + '@sentry/opentelemetry': 10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0) '@sentry/react': 10.34.0(react@19.2.3) '@sentry/vercel-edge': 10.34.0 '@sentry/webpack-plugin': 4.6.2(webpack@5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.18))) - next: 16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) rollup: 4.55.1 stacktrace-parser: 0.1.11 transitivePeerDependencies: @@ -11420,7 +11420,7 @@ snapshots: - supports-color - webpack - '@sentry/node-core@10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)': + '@sentry/node-core@10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0)': dependencies: '@apm-js-collab/tracing-hooks': 0.3.1 '@opentelemetry/api': 1.9.0 @@ -11429,9 +11429,9 @@ snapshots: '@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 '@sentry/core': 10.34.0 - '@sentry/opentelemetry': 10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0) + '@sentry/opentelemetry': 10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0) import-in-the-middle: 2.0.4 transitivePeerDependencies: - supports-color @@ -11466,11 +11466,11 @@ snapshots: '@opentelemetry/instrumentation-undici': 0.19.0(@opentelemetry/api@1.9.0) '@opentelemetry/resources': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 '@prisma/instrumentation': 6.19.0(@opentelemetry/api@1.9.0) '@sentry/core': 10.34.0 - '@sentry/node-core': 10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0) - '@sentry/opentelemetry': 10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0) + '@sentry/node-core': 10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.208.0(@opentelemetry/api@1.9.0))(@opentelemetry/resources@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0) + '@sentry/opentelemetry': 10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0) import-in-the-middle: 2.0.4 minimatch: 9.0.5 transitivePeerDependencies: @@ -11484,13 +11484,13 @@ snapshots: '@sentry/types': 7.120.4 '@sentry/utils': 7.120.4 - '@sentry/opentelemetry@10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.38.0)': + '@sentry/opentelemetry@10.34.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/core@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@2.4.0(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.39.0)': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/core': 2.4.0(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 2.4.0(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.38.0 + '@opentelemetry/semantic-conventions': 1.39.0 '@sentry/core': 10.34.0 '@sentry/react@10.34.0(react@19.2.3)': @@ -11731,7 +11731,7 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@theguild/federation-composition@0.21.2(graphql@16.12.0)': + '@theguild/federation-composition@0.21.3(graphql@16.12.0)': dependencies: constant-case: 3.0.4 debug: 4.4.3 @@ -11781,7 +11781,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 '@types/eslint-scope@3.7.7': dependencies: @@ -11814,7 +11814,7 @@ snapshots: '@types/jsdom@21.1.7': dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 '@types/tough-cookie': 4.0.5 parse5: 7.3.0 @@ -11843,9 +11843,9 @@ snapshots: '@types/mysql@2.15.27': dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 - '@types/node@25.0.8': + '@types/node@25.0.9': dependencies: undici-types: 7.16.0 @@ -11855,7 +11855,7 @@ snapshots: '@types/pg@8.15.6': dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 pg-protocol: 1.11.0 pg-types: 2.2.0 @@ -11871,7 +11871,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 '@types/tough-cookie@4.0.5': {} @@ -11880,7 +11880,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 '@types/yargs-parser@21.0.3': {} @@ -11890,7 +11890,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 optional: true '@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': @@ -12464,7 +12464,7 @@ snapshots: bare-path: 3.0.0 optional: true - baseline-browser-mapping@2.9.14: {} + baseline-browser-mapping@2.9.15: {} basic-ftp@5.1.0: {} @@ -12502,8 +12502,8 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.9.14 - caniuse-lite: 1.0.30001764 + baseline-browser-mapping: 2.9.15 + caniuse-lite: 1.0.30001765 electron-to-chromium: 1.5.267 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -12550,7 +12550,7 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001764: {} + caniuse-lite@1.0.30001765: {} capital-case@1.0.4: dependencies: @@ -12617,7 +12617,7 @@ snapshots: chrome-launcher@0.13.4: dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 escape-string-regexp: 1.0.5 is-wsl: 2.2.0 lighthouse-logger: 1.2.0 @@ -12628,7 +12628,7 @@ snapshots: chrome-launcher@1.2.1: dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 2.0.2 @@ -13119,9 +13119,9 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-next@16.1.2(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + eslint-config-next@16.1.3(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): dependencies: - '@next/eslint-plugin-next': 16.1.2 + '@next/eslint-plugin-next': 16.1.3 eslint: 9.39.2(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) @@ -13210,13 +13210,13 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@29.12.1(@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)))(typescript@5.9.3): + eslint-plugin-jest@29.12.1(@typescript-eslint/eslint-plugin@8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(jest@30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)))(typescript@5.9.3): dependencies: '@typescript-eslint/utils': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.2(jiti@2.6.1) optionalDependencies: '@typescript-eslint/eslint-plugin': 8.53.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - jest: 30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)) + jest: 30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)) transitivePeerDependencies: - supports-color - typescript @@ -13572,9 +13572,9 @@ snapshots: forwarded@0.2.0: {} - framer-motion@12.26.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + framer-motion@12.27.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - motion-dom: 12.26.2 + motion-dom: 12.27.0 motion-utils: 12.24.10 tslib: 2.8.1 optionalDependencies: @@ -13697,13 +13697,13 @@ snapshots: graceful-fs@4.2.11: {} - graphql-config@5.1.5(@types/node@25.0.8)(graphql@16.12.0)(typescript@5.9.3): + graphql-config@5.1.5(@types/node@25.0.9)(graphql@16.12.0)(typescript@5.9.3): dependencies: '@graphql-tools/graphql-file-loader': 8.1.9(graphql@16.12.0) '@graphql-tools/json-file-loader': 8.0.26(graphql@16.12.0) '@graphql-tools/load': 8.1.8(graphql@16.12.0) '@graphql-tools/merge': 9.1.7(graphql@16.12.0) - '@graphql-tools/url-loader': 8.0.33(@types/node@25.0.8)(graphql@16.12.0) + '@graphql-tools/url-loader': 8.0.33(@types/node@25.0.9)(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0) cosmiconfig: 8.3.6(typescript@5.9.3) graphql: 16.12.0 @@ -14178,7 +14178,7 @@ snapshots: '@jest/expect': 30.2.0 '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 chalk: 4.1.2 co: 4.6.0 dedent: 1.7.1 @@ -14198,15 +14198,15 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)): + jest-cli@30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)) + '@jest/core': 30.2.0(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -14217,7 +14217,7 @@ snapshots: - supports-color - ts-node - jest-config@30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)): + jest-config@30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.6 '@jest/get-type': 30.1.0 @@ -14244,8 +14244,8 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 25.0.8 - ts-node: 10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3) + '@types/node': 25.0.9 + ts-node: 10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -14281,7 +14281,7 @@ snapshots: '@jest/environment': 30.2.0 '@jest/environment-jsdom-abstract': 30.2.0(jsdom@26.1.0) '@types/jsdom': 21.1.7 - '@types/node': 25.0.8 + '@types/node': 25.0.9 jsdom: 26.1.0 transitivePeerDependencies: - bufferutil @@ -14293,7 +14293,7 @@ snapshots: '@jest/environment': 30.2.0 '@jest/fake-timers': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 jest-mock: 30.2.0 jest-util: 30.2.0 jest-validate: 30.2.0 @@ -14303,7 +14303,7 @@ snapshots: jest-haste-map@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -14349,7 +14349,7 @@ snapshots: jest-mock@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 jest-util: 30.2.0 jest-pnp-resolver@1.2.3(jest-resolve@30.2.0): @@ -14383,7 +14383,7 @@ snapshots: '@jest/test-result': 30.2.0 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 chalk: 4.1.2 emittery: 0.13.1 exit-x: 0.2.2 @@ -14412,7 +14412,7 @@ snapshots: '@jest/test-result': 30.2.0 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 chalk: 4.1.2 cjs-module-lexer: 2.2.0 collect-v8-coverage: 1.0.3 @@ -14459,7 +14459,7 @@ snapshots: jest-util@30.2.0: dependencies: '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 chalk: 4.1.2 ci-info: 4.3.1 graceful-fs: 4.2.11 @@ -14478,7 +14478,7 @@ snapshots: dependencies: '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 - '@types/node': 25.0.8 + '@types/node': 25.0.9 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -14487,24 +14487,24 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@30.2.0: dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 '@ungap/structured-clone': 1.3.0 jest-util: 30.2.0 merge-stream: 2.0.0 supports-color: 8.1.1 - jest@30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)): + jest@30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)) + '@jest/core': 30.2.0(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)) + jest-cli: 30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -14853,9 +14853,9 @@ snapshots: merge2@1.4.1: {} - meros@1.3.2(@types/node@25.0.8): + meros@1.3.2(@types/node@25.0.9): optionalDependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 metaviewport-parser@0.3.0: {} @@ -14912,7 +14912,7 @@ snapshots: module-details-from-path@1.0.4: {} - motion-dom@12.26.2: + motion-dom@12.27.0: dependencies: motion-utils: 12.24.10 @@ -14940,13 +14940,13 @@ snapshots: netmask@2.0.2: {} - next-auth@4.24.13(next@16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next-auth@4.24.13(next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime': 7.28.6 '@panva/hkdf': 1.2.1 cookie: 0.7.2 jose: 4.15.9 - next: 16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) oauth: 0.9.15 openid-client: 5.7.1 preact: 10.28.2 @@ -14960,25 +14960,25 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - next@16.1.2(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.1.3(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@next/env': 16.1.2 + '@next/env': 16.1.3 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.14 - caniuse-lite: 1.0.30001764 + baseline-browser-mapping: 2.9.15 + caniuse-lite: 1.0.30001765 postcss: 8.4.31 react: 19.2.3 react-dom: 19.2.3(react@19.2.3) styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.3) optionalDependencies: - '@next/swc-darwin-arm64': 16.1.2 - '@next/swc-darwin-x64': 16.1.2 - '@next/swc-linux-arm64-gnu': 16.1.2 - '@next/swc-linux-arm64-musl': 16.1.2 - '@next/swc-linux-x64-gnu': 16.1.2 - '@next/swc-linux-x64-musl': 16.1.2 - '@next/swc-win32-arm64-msvc': 16.1.2 - '@next/swc-win32-x64-msvc': 16.1.2 + '@next/swc-darwin-arm64': 16.1.3 + '@next/swc-darwin-x64': 16.1.3 + '@next/swc-linux-arm64-gnu': 16.1.3 + '@next/swc-linux-arm64-musl': 16.1.3 + '@next/swc-linux-x64-gnu': 16.1.3 + '@next/swc-linux-x64-musl': 16.1.3 + '@next/swc-win32-arm64-msvc': 16.1.3 + '@next/swc-win32-x64-msvc': 16.1.3 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.57.0 sharp: 0.34.5 @@ -15859,7 +15859,7 @@ snapshots: speedline-core@1.4.3: dependencies: - '@types/node': 25.0.8 + '@types/node': 25.0.9 image-ssim: 0.2.0 jpeg-js: 0.4.4 @@ -16094,12 +16094,12 @@ snapshots: jest-worker: 27.5.1 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - terser: 5.45.0 + terser: 5.46.0 webpack: 5.103.0(@swc/core@1.15.8(@swc/helpers@0.5.18)) optionalDependencies: '@swc/core': 1.15.8(@swc/helpers@0.5.18) - terser@5.45.0: + terser@5.46.0: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.15.0 @@ -16183,12 +16183,12 @@ snapshots: dependencies: typescript: 5.9.3 - ts-jest@29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.6(@babel/core@7.28.6)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.6))(jest-util@30.2.0)(jest@30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.2.0(@types/node@25.0.8)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3)) + jest: 30.2.0(@types/node@25.0.9)(ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -16205,14 +16205,14 @@ snapshots: ts-log@2.2.7: {} - ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.8)(typescript@5.9.3): + ts-node@10.9.2(@swc/core@1.15.8(@swc/helpers@0.5.18))(@types/node@25.0.9)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 25.0.8 + '@types/node': 25.0.9 acorn: 8.15.0 acorn-walk: 8.3.4 arg: 4.1.3 diff --git a/frontend/src/app/board/[year]/candidates/layout.tsx b/frontend/src/app/board/[year]/candidates/layout.tsx index c2999f97a5..1d8917f8d0 100644 --- a/frontend/src/app/board/[year]/candidates/layout.tsx +++ b/frontend/src/app/board/[year]/candidates/layout.tsx @@ -6,6 +6,8 @@ export const metadata: Metadata = { description: 'OWASP Board of Directors election candidates', } -export default function BoardCandidatesLayout({ children }: { children: React.ReactNode }) { +export default function BoardCandidatesLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { return <>{children} } diff --git a/frontend/src/components/ChapterMap.tsx b/frontend/src/components/ChapterMap.tsx index 4c36832d77..cd45508c10 100644 --- a/frontend/src/components/ChapterMap.tsx +++ b/frontend/src/components/ChapterMap.tsx @@ -10,7 +10,6 @@ import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet' import MarkerClusterGroup from 'react-leaflet-cluster' import type { Chapter } from 'types/chapter' import type { UserLocation } from 'utils/geolocationUtils' -import 'leaflet.markercluster' import 'leaflet/dist/leaflet.css' import 'leaflet.markercluster/dist/MarkerCluster.css' import 'leaflet.markercluster/dist/MarkerCluster.Default.css' @@ -22,12 +21,22 @@ const MapZoomControl = ({ isMapActive }: { isMapActive: boolean }) => { if (!map) return if (isMapActive) { map.scrollWheelZoom.enable() + map.dragging.enable() + map.touchZoom.enable() + map.doubleClickZoom.enable() + map.keyboard.enable() + if (!zoomControlRef.current) { zoomControlRef.current = L.control.zoom({ position: 'topleft' }) zoomControlRef.current.addTo(map) } } else { map.scrollWheelZoom.disable() + map.dragging.disable() + map.touchZoom.disable() + map.doubleClickZoom.disable() + map.keyboard.disable() + if (zoomControlRef.current) { zoomControlRef.current.remove() zoomControlRef.current = null @@ -39,6 +48,10 @@ const MapZoomControl = ({ isMapActive }: { isMapActive: boolean }) => { return () => { if (!map) return map.scrollWheelZoom.disable() + map.dragging.disable() + map.touchZoom.disable() + map.doubleClickZoom.disable() + map.keyboard.disable() if (zoomControlRef.current) { zoomControlRef.current.remove() zoomControlRef.current = null @@ -61,6 +74,14 @@ const MapViewUpdater = ({ useEffect(() => { if (!map) return + const container = map.getContainer() + const width = container.clientWidth + const height = container.clientHeight + const aspectRatio = height > 0 ? width / height : 1 + + const dynamicMinZoom = aspectRatio > 2 ? 1 : 2 + map.setMinZoom(dynamicMinZoom) + if (userLocation && validGeoLocData.length > 0) { const maxNearestChapters = 5 const localChapters = validGeoLocData.slice(0, maxNearestChapters) @@ -76,7 +97,8 @@ const MapViewUpdater = ({ ] const localBounds = L.latLngBounds(locationsForBounds) const maxZoom = 12 - map.fitBounds(localBounds, { maxZoom: maxZoom }) + const padding = 50 + map.fitBounds(localBounds, { maxZoom: maxZoom, padding: [padding, padding] }) } else if (showLocal && validGeoLocData.length > 0) { const maxNearestChapters = 5 const localChapters = validGeoLocData.slice(0, maxNearestChapters - 1) @@ -87,6 +109,7 @@ const MapViewUpdater = ({ ]) ) const maxZoom = 7 + const padding = 50 const nearestChapter = validGeoLocData[0] map.setView( [ @@ -95,9 +118,9 @@ const MapViewUpdater = ({ ], maxZoom ) - map.fitBounds(localBounds, { maxZoom: maxZoom }) + map.fitBounds(localBounds, { maxZoom: maxZoom, padding: [padding, padding] }) } else { - map.setView([20, 0], 2) + map.setView([20, 0], Math.max(dynamicMinZoom, 2)) } }, [userLocation, showLocal, validGeoLocData, map]) @@ -167,11 +190,14 @@ const ChapterMap = ({ scrollWheelZoom={isMapActive} style={{ height: '100%', width: '100%' }} zoomControl={false} + minZoom={1} + maxZoom={18} + worldCopyJump={true} maxBounds={[ - [-90, -180], - [90, 180], + [-85, -180], + [85, 180], ]} - maxBoundsViscosity={1} + maxBoundsViscosity={0.5} > {!isMapActive && ( - + + )} {isMapActive && (
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index da728e905c..f01500362f 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -1,6 +1,5 @@ 'use client' import { Button } from '@heroui/button' -import { useIsMobile } from 'hooks/useIsMobile' import Image from 'next/image' import Link from 'next/link' import { usePathname } from 'next/navigation' @@ -22,7 +21,6 @@ import UserMenu from 'components/UserMenu' export default function Header({ isGitHubAuthEnabled }: { readonly isGitHubAuthEnabled: boolean }) { const pathname = usePathname() - const isMobile = useIsMobile() const [mobileMenuOpen, setMobileMenuOpen] = useState(false) const toggleMobileMenu = () => setMobileMenuOpen(!mobileMenuOpen) @@ -58,7 +56,7 @@ export default function Header({ isGitHubAuthEnabled }: { readonly isGitHubAuthE return (
-