diff --git a/.github/DISCUSSION_TEMPLATE/feature-request.yaml b/.github/DISCUSSION_TEMPLATE/feature-request.yaml index 9aeee8004c..7a260188ea 100644 --- a/.github/DISCUSSION_TEMPLATE/feature-request.yaml +++ b/.github/DISCUSSION_TEMPLATE/feature-request.yaml @@ -11,7 +11,7 @@ body: - type: checkboxes attributes: - label: I have searched the existing feature requests to make sure this is not a duplicate request. + label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request. options: - label: "Yes" required: true diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c7519a4684..acbb7c785b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: ['https://buy.immich.app'] +custom: ['https://buy.immich.app', 'https://immich.store'] diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 346c6e60f2..c4e1cc2bf1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -1,6 +1,13 @@ name: Report an issue with Immich description: Report an issue with Immich body: + - type: checkboxes + attributes: + label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report. + options: + - label: "Yes" + required: true + - type: markdown attributes: value: | diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 02930a2c2f..7cfd75c61b 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -56,10 +56,10 @@ jobs: uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v3.4.0 + uses: docker/setup-qemu-action@v3.5.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.9.0 + uses: docker/setup-buildx-action@v3.10.0 - name: Login to GitHub Container Registry uses: docker/login-action@v3 @@ -88,7 +88,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@v6.13.0 + uses: docker/build-push-action@v6.15.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d89a08cb40..96970fa460 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -5,7 +5,6 @@ on: push: branches: [main] pull_request: - branches: [main] release: types: [published] @@ -141,7 +140,7 @@ jobs: uses: actions/checkout@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3.9.0 + uses: docker/setup-buildx-action@v3.10.0 - name: Login to GitHub Container Registry uses: docker/login-action@v3 @@ -171,7 +170,7 @@ jobs: - name: Build and push image id: build - uses: docker/build-push-action@v6.13.0 + uses: docker/build-push-action@v6.15.0 with: context: ${{ env.context }} file: ${{ env.file }} @@ -334,7 +333,7 @@ jobs: - name: Build and push image id: build - uses: docker/build-push-action@v6.13.0 + uses: docker/build-push-action@v6.15.0 with: context: ${{ env.context }} file: ${{ env.file }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e80b6aabb0..476ef9f354 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -457,7 +457,7 @@ jobs: runs-on: ubuntu-latest services: postgres: - image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml new file mode 100644 index 0000000000..29a30640bd --- /dev/null +++ b/.github/workflows/weblate-lock.yml @@ -0,0 +1,50 @@ +name: Weblate checks + +on: + pull_request: + branches: [main] + +jobs: + pre-job: + runs-on: ubuntu-latest + outputs: + should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - id: found_paths + uses: dorny/paths-filter@v3 + with: + filters: | + i18n: + - 'i18n/!(en)**\.json' + enforce-lock: + name: Check Weblate Lock + runs-on: ubuntu-latest + if: ${{ needs.pre-job.outputs.should_run == 'true' }} + steps: + - name: Check weblate lock + run: | + if [[ "false" = $(curl https://hosted.weblate.org/api/components/immich/immich/lock/ | jq .locked) ]]; then + exit 1 + fi + - name: Find Pull Request + uses: juliangruber/find-pull-request-action@v1 + id: find-pr + with: + branch: chore/translations + - name: Fail if existing weblate PR + if: ${{ steps.find-pr.outputs.number }} + run: exit 1 + success-check-lock: + name: Weblate Lock Check Success + needs: [ enforce-lock ] + runs-on: ubuntu-latest + if: always() + steps: + - name: Any jobs failed? + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 + - name: All jobs passed or skipped + if: ${{ !(contains(needs.*.result, 'failure')) }} + run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" diff --git a/cli/Dockerfile b/cli/Dockerfile index 6ddceafb59..356537213b 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.13.1-alpine3.20@sha256:c52e20859a92b3eccbd3a36c5e1a90adc20617d8d421d65e8a622e87b5dac963 AS core +FROM node:22.14.0-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS core WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/cli/package-lock.json b/cli/package-lock.json index 3d8cf35459..2b40efdb45 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,17 +1,19 @@ { "name": "@immich/cli", - "version": "2.2.50", + "version": "2.2.53", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.50", + "version": "2.2.53", "license": "GNU Affero General Public License version 3", "dependencies": { + "chokidar": "^4.0.3", "fast-glob": "^3.3.2", "fastq": "^1.17.1", - "lodash-es": "^4.17.21" + "lodash-es": "^4.17.21", + "micromatch": "^4.0.8" }, "bin": { "immich": "dist/index.js" @@ -23,8 +25,9 @@ "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", + "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^3.0.0", @@ -35,7 +38,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^56.0.1", - "globals": "^15.9.0", + "globals": "^16.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", @@ -52,14 +55,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.126.1", + "version": "1.129.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "typescript": "^5.3.3" } }, @@ -318,9 +321,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", "cpu": [ "ppc64" ], @@ -335,9 +338,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", "cpu": [ "arm" ], @@ -352,9 +355,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", "cpu": [ "arm64" ], @@ -369,9 +372,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", "cpu": [ "x64" ], @@ -386,9 +389,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", "cpu": [ "arm64" ], @@ -403,9 +406,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", "cpu": [ "x64" ], @@ -420,9 +423,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", "cpu": [ "arm64" ], @@ -437,9 +440,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", "cpu": [ "x64" ], @@ -454,9 +457,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", "cpu": [ "arm" ], @@ -471,9 +474,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", "cpu": [ "arm64" ], @@ -488,9 +491,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", "cpu": [ "ia32" ], @@ -505,9 +508,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", "cpu": [ "loong64" ], @@ -522,9 +525,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", "cpu": [ "mips64el" ], @@ -539,9 +542,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", "cpu": [ "ppc64" ], @@ -556,9 +559,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", "cpu": [ "riscv64" ], @@ -573,9 +576,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", "cpu": [ "s390x" ], @@ -590,9 +593,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", "cpu": [ "x64" ], @@ -607,9 +610,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", "cpu": [ "arm64" ], @@ -624,9 +627,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", "cpu": [ "x64" ], @@ -641,9 +644,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", "cpu": [ "arm64" ], @@ -658,9 +661,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", "cpu": [ "x64" ], @@ -675,9 +678,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", "cpu": [ "x64" ], @@ -692,9 +695,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", "cpu": [ "arm64" ], @@ -709,9 +712,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", "cpu": [ "ia32" ], @@ -726,9 +729,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", "cpu": [ "x64" ], @@ -768,13 +771,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -807,9 +810,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -820,9 +823,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -881,9 +884,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", "dev": true, "license": "MIT", "engines": { @@ -891,9 +894,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -901,13 +904,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.12.0", "levn": "^0.4.1" }, "engines": { @@ -966,9 +969,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1427,6 +1430,13 @@ "win32" ] }, + "node_modules/@types/braces": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.4.tgz", + "integrity": "sha512-0WR3b8eaISjEW7RpZnclONaLFDf7buaowRHdqLp4vLj54AsSAYWfh3DRbfiYJY9XDxMgx1B4sE1Afw2PGpuHOA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/byte-size": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz", @@ -1472,6 +1482,16 @@ "@types/lodash": "*" } }, + "node_modules/@types/micromatch": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.9.tgz", + "integrity": "sha512-7V+8ncr22h4UoYRLnLXSpTxjQrNUXtWHGeMPRJt1nULXI57G9bIcpyrHlmrQ7QK24EyyuXvYcSSWAM8GA9nqCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/braces": "*" + } + }, "node_modules/@types/mock-fs": { "version": "4.13.4", "resolved": "https://registry.npmjs.org/@types/mock-fs/-/mock-fs-4.13.4.tgz", @@ -1482,9 +1502,9 @@ } }, "node_modules/@types/node": { - "version": "22.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", - "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "version": "22.13.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", + "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1498,17 +1518,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", - "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz", + "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/type-utils": "8.24.0", - "@typescript-eslint/utils": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/type-utils": "8.25.0", + "@typescript-eslint/utils": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1528,16 +1548,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", - "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz", + "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/typescript-estree": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "engines": { @@ -1553,14 +1573,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", - "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz", + "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0" + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1571,14 +1591,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", - "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz", + "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/utils": "8.24.0", + "@typescript-eslint/typescript-estree": "8.25.0", + "@typescript-eslint/utils": "8.25.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1595,9 +1615,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", - "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", + "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", "dev": true, "license": "MIT", "engines": { @@ -1609,14 +1629,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", - "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz", + "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -1636,16 +1656,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", - "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz", + "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0" + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/typescript-estree": "8.25.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1660,13 +1680,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", - "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz", + "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -1691,9 +1711,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz", - "integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.7.tgz", + "integrity": "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1714,8 +1734,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.5", - "vitest": "3.0.5" + "@vitest/browser": "3.0.7", + "vitest": "3.0.7" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1724,15 +1744,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", - "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, "funding": { @@ -1740,13 +1760,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", - "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", + "@vitest/spy": "3.0.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -1767,9 +1787,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", - "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", "dev": true, "license": "MIT", "dependencies": { @@ -1780,38 +1800,38 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", - "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.5", - "pathe": "^2.0.2" + "@vitest/utils": "3.0.7", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", - "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", + "@vitest/pretty-format": "3.0.7", "magic-string": "^0.30.17", - "pathe": "^2.0.2" + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", - "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", "dev": true, "license": "MIT", "dependencies": { @@ -1822,14 +1842,14 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", - "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", - "loupe": "^3.1.2", + "@vitest/pretty-format": "3.0.7", + "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, "funding": { @@ -2047,9 +2067,9 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", "dependencies": { @@ -2089,6 +2109,21 @@ "node": ">= 16" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/ci-info": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", @@ -2271,9 +2306,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2284,31 +2319,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" } }, "node_modules/escalade": { @@ -2334,22 +2369,22 @@ } }, "node_modules/eslint": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", - "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", + "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.19.2", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.21.0", + "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -2394,9 +2429,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", - "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.2.tgz", + "integrity": "sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==", "dev": true, "license": "MIT", "bin": { @@ -2471,6 +2506,19 @@ "eslint": ">=8.56.0" } }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", @@ -2500,19 +2548,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2698,9 +2733,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", - "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -2841,9 +2876,9 @@ } }, "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", "dev": true, "license": "MIT", "engines": { @@ -3500,9 +3535,9 @@ } }, "node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -3544,9 +3579,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -3582,9 +3617,9 @@ } }, "node_modules/prettier": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", - "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", + "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", "dev": true, "license": "MIT", "bin": { @@ -3756,6 +3791,19 @@ "node": ">=8" } }, + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "license": "MIT", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -4301,14 +4349,14 @@ } }, "node_modules/vite": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", - "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.5.1", + "esbuild": "^0.25.0", + "postcss": "^8.5.3", "rollup": "^4.30.1" }, "bin": { @@ -4373,16 +4421,16 @@ } }, "node_modules/vite-node": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", - "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", - "pathe": "^2.0.2", + "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { @@ -4416,31 +4464,31 @@ } }, "node_modules/vitest": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", - "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.5", - "@vitest/mocker": "3.0.5", - "@vitest/pretty-format": "^3.0.5", - "@vitest/runner": "3.0.5", - "@vitest/snapshot": "3.0.5", - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", - "pathe": "^2.0.2", + "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.5", + "vite-node": "3.0.7", "why-is-node-running": "^2.3.0" }, "bin": { @@ -4456,8 +4504,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.5", - "@vitest/ui": "3.0.5", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", "happy-dom": "*", "jsdom": "*" }, @@ -4486,9 +4534,9 @@ } }, "node_modules/vitest-fetch-mock": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.4.3.tgz", - "integrity": "sha512-PhuEh+9HCsXFMRPUJilDL7yVDFufoxqk7ze+CNks64UGlfFXaJTn1bLABiNlEc0u25RERXQGj0Tm+M9i6UY9HQ==", + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.4.4.tgz", + "integrity": "sha512-i2RNEAKBgnLWwj5DVz8ouzaHaPVg1xaYgAUmU5p+baJ149upnO+yJLPchAiY9ij8hf0PDkJVVke1pftBxmT05g==", "dev": true, "license": "MIT", "engines": { diff --git a/cli/package.json b/cli/package.json index 2f0ddde3e4..e8c207fb27 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.50", + "version": "2.2.53", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -19,8 +19,9 @@ "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", + "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^3.0.0", @@ -31,7 +32,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^56.0.1", - "globals": "^15.9.0", + "globals": "^16.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", @@ -62,9 +63,11 @@ "node": ">=20.0.0" }, "dependencies": { + "chokidar": "^4.0.3", "fast-glob": "^3.3.2", "fastq": "^1.17.1", - "lodash-es": "^4.17.21" + "lodash-es": "^4.17.21", + "micromatch": "^4.0.8" }, "volta": { "node": "22.14.0" diff --git a/cli/src/commands/asset.spec.ts b/cli/src/commands/asset.spec.ts index 4bac1d00ab..21137a3296 100644 --- a/cli/src/commands/asset.spec.ts +++ b/cli/src/commands/asset.spec.ts @@ -1,12 +1,13 @@ import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; -import { describe, expect, it, vi } from 'vitest'; +import { setTimeout as sleep } from 'node:timers/promises'; +import { describe, expect, it, MockedFunction, vi } from 'vitest'; -import { Action, checkBulkUpload, defaults, Reason } from '@immich/sdk'; +import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk'; import createFetchMock from 'vitest-fetch-mock'; -import { checkForDuplicates, getAlbumName, uploadFiles, UploadOptionsDto } from './asset'; +import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset'; vi.mock('@immich/sdk'); @@ -199,3 +200,112 @@ describe('checkForDuplicates', () => { }); }); }); + +describe('startWatch', () => { + let testFolder: string; + let checkBulkUploadMocked: MockedFunction; + + beforeEach(async () => { + vi.restoreAllMocks(); + + vi.mocked(getSupportedMediaTypes).mockResolvedValue({ + image: ['.jpg'], + sidecar: ['.xmp'], + video: ['.mp4'], + }); + + testFolder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'test-startWatch-')); + checkBulkUploadMocked = vi.mocked(checkBulkUpload); + checkBulkUploadMocked.mockResolvedValue({ + results: [], + }); + }); + + it('should start watching a directory and upload new files', async () => { + const testFilePath = path.join(testFolder, 'test.jpg'); + + await startWatch([testFolder], { concurrency: 1 }, { batchSize: 1, debounceTimeMs: 10 }); + await sleep(100); // to debounce the watcher from considering the test file as a existing file + await fs.promises.writeFile(testFilePath, 'testjpg'); + + await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000); + expect(checkBulkUpload).toHaveBeenCalledWith({ + assetBulkUploadCheckDto: { + assets: [ + expect.objectContaining({ + id: testFilePath, + }), + ], + }, + }); + }); + + it('should filter out unsupported files', async () => { + const testFilePath = path.join(testFolder, 'test.jpg'); + const unsupportedFilePath = path.join(testFolder, 'test.txt'); + + await startWatch([testFolder], { concurrency: 1 }, { batchSize: 1, debounceTimeMs: 10 }); + await sleep(100); // to debounce the watcher from considering the test file as a existing file + await fs.promises.writeFile(testFilePath, 'testjpg'); + await fs.promises.writeFile(unsupportedFilePath, 'testtxt'); + + await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000); + expect(checkBulkUpload).toHaveBeenCalledWith({ + assetBulkUploadCheckDto: { + assets: expect.arrayContaining([ + expect.objectContaining({ + id: testFilePath, + }), + ]), + }, + }); + + expect(checkBulkUpload).not.toHaveBeenCalledWith({ + assetBulkUploadCheckDto: { + assets: expect.arrayContaining([ + expect.objectContaining({ + id: unsupportedFilePath, + }), + ]), + }, + }); + }); + + it('should filger out ignored patterns', async () => { + const testFilePath = path.join(testFolder, 'test.jpg'); + const ignoredPattern = 'ignored'; + const ignoredFolder = path.join(testFolder, ignoredPattern); + await fs.promises.mkdir(ignoredFolder, { recursive: true }); + const ignoredFilePath = path.join(ignoredFolder, 'ignored.jpg'); + + await startWatch([testFolder], { concurrency: 1, ignore: ignoredPattern }, { batchSize: 1, debounceTimeMs: 10 }); + await sleep(100); // to debounce the watcher from considering the test file as a existing file + await fs.promises.writeFile(testFilePath, 'testjpg'); + await fs.promises.writeFile(ignoredFilePath, 'ignoredjpg'); + + await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000); + expect(checkBulkUpload).toHaveBeenCalledWith({ + assetBulkUploadCheckDto: { + assets: expect.arrayContaining([ + expect.objectContaining({ + id: testFilePath, + }), + ]), + }, + }); + + expect(checkBulkUpload).not.toHaveBeenCalledWith({ + assetBulkUploadCheckDto: { + assets: expect.arrayContaining([ + expect.objectContaining({ + id: ignoredFilePath, + }), + ]), + }, + }); + }); + + afterEach(async () => { + await fs.promises.rm(testFolder, { recursive: true, force: true }); + }); +}); diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index 4cf6742f24..d06b30e984 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -12,13 +12,18 @@ import { getSupportedMediaTypes, } from '@immich/sdk'; import byteSize from 'byte-size'; +import { Matcher, watch as watchFs } from 'chokidar'; import { MultiBar, Presets, SingleBar } from 'cli-progress'; import { chunk } from 'lodash-es'; +import micromatch from 'micromatch'; import { Stats, createReadStream } from 'node:fs'; import { stat, unlink } from 'node:fs/promises'; import path, { basename } from 'node:path'; import { Queue } from 'src/queue'; -import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils'; +import { BaseOptions, Batcher, authenticate, crawl, sha1 } from 'src/utils'; + +const UPLOAD_WATCH_BATCH_SIZE = 100; +const UPLOAD_WATCH_DEBOUNCE_TIME_MS = 10_000; const s = (count: number) => (count === 1 ? '' : 's'); @@ -36,6 +41,8 @@ export interface UploadOptionsDto { albumName?: string; includeHidden?: boolean; concurrency: number; + progress?: boolean; + watch?: boolean; } class UploadFile extends File { @@ -55,19 +62,94 @@ class UploadFile extends File { } } +const uploadBatch = async (files: string[], options: UploadOptionsDto) => { + const { newFiles, duplicates } = await checkForDuplicates(files, options); + const newAssets = await uploadFiles(newFiles, options); + await updateAlbums([...newAssets, ...duplicates], options); + await deleteFiles(newFiles, options); +}; + +export const startWatch = async ( + paths: string[], + options: UploadOptionsDto, + { + batchSize = UPLOAD_WATCH_BATCH_SIZE, + debounceTimeMs = UPLOAD_WATCH_DEBOUNCE_TIME_MS, + }: { batchSize?: number; debounceTimeMs?: number } = {}, +) => { + const watcherIgnored: Matcher[] = []; + const { image, video } = await getSupportedMediaTypes(); + const extensions = new Set([...image, ...video]); + + if (options.ignore) { + watcherIgnored.push((path) => micromatch.contains(path, `**/${options.ignore}`)); + } + + const pathsBatcher = new Batcher({ + batchSize, + debounceTimeMs, + onBatch: async (paths: string[]) => { + const uniquePaths = [...new Set(paths)]; + await uploadBatch(uniquePaths, options); + }, + }); + + const onFile = async (path: string, stats?: Stats) => { + if (stats?.isDirectory()) { + return; + } + const ext = '.' + path.split('.').pop()?.toLowerCase(); + if (!ext || !extensions.has(ext)) { + return; + } + + if (!options.progress) { + // logging when progress is disabled as it can cause issues with the progress bar rendering + console.log(`Change detected: ${path}`); + } + pathsBatcher.add(path); + }; + const fsWatcher = watchFs(paths, { + ignoreInitial: true, + ignored: watcherIgnored, + alwaysStat: true, + awaitWriteFinish: true, + depth: options.recursive ? undefined : 1, + persistent: true, + }) + .on('add', onFile) + .on('change', onFile) + .on('error', (error) => console.error(`Watcher error: ${error}`)); + + process.on('SIGINT', async () => { + console.log('Exiting...'); + await fsWatcher.close(); + process.exit(); + }); +}; + export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => { await authenticate(baseOptions); const scanFiles = await scan(paths, options); + if (scanFiles.length === 0) { - console.log('No files found, exiting'); - return; + if (options.watch) { + console.log('No files found initially.'); + } else { + console.log('No files found, exiting'); + return; + } } - const { newFiles, duplicates } = await checkForDuplicates(scanFiles, options); - const newAssets = await uploadFiles(newFiles, options); - await updateAlbums([...newAssets, ...duplicates], options); - await deleteFiles(newFiles, options); + if (options.watch) { + console.log('Watching for changes...'); + await startWatch(paths, options); + // watcher does not handle the initial scan + // as the scan() is a more efficient quick start with batched results + } + + await uploadBatch(scanFiles, options); }; const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => { @@ -85,19 +167,25 @@ const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => { return files; }; -export const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => { +export const checkForDuplicates = async (files: string[], { concurrency, skipHash, progress }: UploadOptionsDto) => { if (skipHash) { console.log('Skipping hash check, assuming all files are new'); return { newFiles: files, duplicates: [] }; } - const multiBar = new MultiBar( - { format: '{message} | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' }, - Presets.shades_classic, - ); + let multiBar: MultiBar | undefined; - const hashProgressBar = multiBar.create(files.length, 0, { message: 'Hashing files ' }); - const checkProgressBar = multiBar.create(files.length, 0, { message: 'Checking for duplicates' }); + if (progress) { + multiBar = new MultiBar( + { format: '{message} | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' }, + Presets.shades_classic, + ); + } else { + console.log(`Received ${files.length} files, hashing...`); + } + + const hashProgressBar = multiBar?.create(files.length, 0, { message: 'Hashing files ' }); + const checkProgressBar = multiBar?.create(files.length, 0, { message: 'Checking for duplicates' }); const newFiles: string[] = []; const duplicates: Asset[] = []; @@ -117,7 +205,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas } } - checkProgressBar.increment(assets.length); + checkProgressBar?.increment(assets.length); }, { concurrency, retry: 3 }, ); @@ -137,7 +225,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas void checkBulkUploadQueue.push(batch); } - hashProgressBar.increment(); + hashProgressBar?.increment(); return results; }, { concurrency, retry: 3 }, @@ -155,7 +243,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas await checkBulkUploadQueue.drained(); - multiBar.stop(); + multiBar?.stop(); console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`); @@ -171,7 +259,10 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas return { newFiles, duplicates }; }; -export const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise => { +export const uploadFiles = async ( + files: string[], + { dryRun, concurrency, progress }: UploadOptionsDto, +): Promise => { if (files.length === 0) { console.log('All assets were already uploaded, nothing to do.'); return []; @@ -191,12 +282,20 @@ export const uploadFiles = async (files: string[], { dryRun, concurrency }: Uplo return files.map((filepath) => ({ id: '', filepath })); } - const uploadProgress = new SingleBar( - { format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}' }, - Presets.shades_classic, - ); - uploadProgress.start(totalSize, 0); - uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) }); + let uploadProgress: SingleBar | undefined; + + if (progress) { + uploadProgress = new SingleBar( + { + format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}', + }, + Presets.shades_classic, + ); + } else { + console.log(`Uploading ${files.length} asset${s(files.length)} (${byteSize(totalSize)})`); + } + uploadProgress?.start(totalSize, 0); + uploadProgress?.update({ value_formatted: 0, total_formatted: byteSize(totalSize) }); let duplicateCount = 0; let duplicateSize = 0; @@ -222,7 +321,7 @@ export const uploadFiles = async (files: string[], { dryRun, concurrency }: Uplo successSize += stats.size ?? 0; } - uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) }); + uploadProgress?.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) }); return response; }, @@ -235,7 +334,7 @@ export const uploadFiles = async (files: string[], { dryRun, concurrency }: Uplo await queue.drained(); - uploadProgress.stop(); + uploadProgress?.stop(); console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`); if (duplicateCount > 0) { diff --git a/cli/src/index.ts b/cli/src/index.ts index 341a70bef0..5da4b50722 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -69,6 +69,13 @@ program .default(4), ) .addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS')) + .addOption(new Option('--no-progress', 'Hide progress bars').env('IMMICH_PROGRESS_BAR').default(true)) + .addOption( + new Option('--watch', 'Watch for changes and upload automatically') + .env('IMMICH_WATCH_CHANGES') + .default(false) + .implies({ progress: false }), + ) .argument('[paths...]', 'One or more paths to assets to be uploaded') .action((paths, options) => upload(paths, program.opts(), options)); diff --git a/cli/src/utils.spec.ts b/cli/src/utils.spec.ts index 93f031872b..5dd28a55e3 100644 --- a/cli/src/utils.spec.ts +++ b/cli/src/utils.spec.ts @@ -1,6 +1,7 @@ import mockfs from 'mock-fs'; import { readFileSync } from 'node:fs'; -import { CrawlOptions, crawl } from 'src/utils'; +import { Batcher, CrawlOptions, crawl } from 'src/utils'; +import { Mock } from 'vitest'; interface Test { test: string; @@ -303,3 +304,38 @@ describe('crawl', () => { } }); }); + +describe('Batcher', () => { + let batcher: Batcher; + let onBatch: Mock; + beforeEach(() => { + onBatch = vi.fn(); + batcher = new Batcher({ batchSize: 2, onBatch }); + }); + + it('should trigger onBatch() when a batch limit is reached', async () => { + batcher.add('a'); + batcher.add('b'); + batcher.add('c'); + expect(onBatch).toHaveBeenCalledOnce(); + expect(onBatch).toHaveBeenCalledWith(['a', 'b']); + }); + + it('should trigger onBatch() when flush() is called', async () => { + batcher.add('a'); + batcher.flush(); + expect(onBatch).toHaveBeenCalledOnce(); + expect(onBatch).toHaveBeenCalledWith(['a']); + }); + + it('should trigger onBatch() when debounce time reached', async () => { + vi.useFakeTimers(); + batcher = new Batcher({ batchSize: 2, debounceTimeMs: 100, onBatch }); + batcher.add('a'); + expect(onBatch).not.toHaveBeenCalled(); + vi.advanceTimersByTime(200); + expect(onBatch).toHaveBeenCalledOnce(); + expect(onBatch).toHaveBeenCalledWith(['a']); + vi.useRealTimers(); + }); +}); diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 27cc2f9e08..eae5164394 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -172,3 +172,64 @@ export const sha1 = (filepath: string) => { rs.on('end', () => resolve(hash.digest('hex'))); }); }; + +/** + * Batches items and calls onBatch to process them + * when the batch size is reached or the debounce time has passed. + */ +export class Batcher { + private items: T[] = []; + private readonly batchSize: number; + private readonly debounceTimeMs?: number; + private readonly onBatch: (items: T[]) => void; + private debounceTimer?: NodeJS.Timeout; + + constructor({ + batchSize, + debounceTimeMs, + onBatch, + }: { + batchSize: number; + debounceTimeMs?: number; + onBatch: (items: T[]) => Promise; + }) { + this.batchSize = batchSize; + this.debounceTimeMs = debounceTimeMs; + this.onBatch = onBatch; + } + + private setDebounceTimer() { + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + } + if (this.debounceTimeMs) { + this.debounceTimer = setTimeout(() => this.flush(), this.debounceTimeMs); + } + } + + private clearDebounceTimer() { + if (this.debounceTimer) { + clearTimeout(this.debounceTimer); + this.debounceTimer = undefined; + } + } + + add(item: T) { + this.items.push(item); + this.setDebounceTimer(); + if (this.items.length >= this.batchSize) { + this.flush(); + } + } + + flush() { + this.clearDebounceTimer(); + if (this.items.length === 0) { + return; + } + + this.onBatch(this.items); + + this.items = []; + } +} diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index ad282ed24b..9df5be3901 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -122,7 +122,7 @@ services: database: container_name: immich_postgres - image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 env_file: - .env environment: diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index ffc5c85b1d..559dd55e72 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -63,7 +63,7 @@ services: database: container_name: immich_postgres - image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 env_file: - .env environment: @@ -100,7 +100,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:5888c188cf09e3f7eebc97369c3b2ce713e844cdbd88ccf36f5047c958aea120 + image: prom/prometheus@sha256:6927e0919a144aa7616fd0137d4816816d42f6b816de3af269ab065250859a62 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus @@ -112,7 +112,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:11.5.1-ubuntu@sha256:9a4ab78cec1a2ec7d1ca5dfd5aacec6412706a1bc9e971fc7184e2f6696a63f5 + image: grafana/grafana:11.5.2-ubuntu@sha256:8b5858c447e06fd7a89006b562ba7bba7c4d5813600c7982374c41852adefaeb volumes: - grafana-data:/var/lib/grafana diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 08437e17c7..fd0edf9cb0 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -56,7 +56,7 @@ services: database: container_name: immich_postgres - image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 + image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 4cd3717e84..23b2b9b30f 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -97,7 +97,7 @@ Make sure to [set your reverse proxy](/docs/administration/reverse-proxy/) to al Also, check the disk space of your reverse proxy. In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails. -If you are using Cloudflare Tunnel, please know that they set a maxiumum filesize of 100 MB that cannot be changed. +If you are using Cloudflare Tunnel, please know that they set a maximum filesize of 100 MB that cannot be changed. At times, files larger than this may work, potentially up to 1 GB. However, the official limit is 100 MB. If you are having issues, we recommend switching to a different network deployment. @@ -170,7 +170,7 @@ If you aren't able to or prefer not to mount Samba on the host (such as Windows Below is an example in the `docker-compose.yml`. Change your username, password, local IP, and share name, and see below where the line `- originals:/usr/src/app/originals`, -corrolates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like. +correlates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like. For example you could change `originals:` to `Photos:`, and change `- originals:/usr/src/app/originals` to `Photos:/usr/src/app/photos`. ```diff diff --git a/docs/docs/administration/system-settings.md b/docs/docs/administration/system-settings.md index 92b910a01b..f241050136 100644 --- a/docs/docs/administration/system-settings.md +++ b/docs/docs/administration/system-settings.md @@ -98,6 +98,14 @@ The default Immich log level is `Log` (commonly known as `Info`). The Immich adm Through this setting, you can manage all the settings related to machine learning in Immich, from the setting of remote machine learning to the model and its parameters You can choose to disable a certain type of machine learning, for example smart search or facial recognition. +### URL + +The built in (`http://immich-machine-learning:3003`) machine learning server will be configured by default, but you can change this or add additional servers. + +Hosting the `immich-machine-learning` container on a machine with a more powerful GPU can be helpful to for processing a large number of photos (such as during batch import) or for faster search. + +If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online. + ### Smart Search The [smart search](/docs/features/searching) settings allow you to change the [CLIP model](https://openai.com/research/clip). Larger models will typically provide [more accurate search results](https://github.com/immich-app/immich/discussions/11862) but consume more processing power and RAM. When [changing the CLIP model](/docs/FAQ#can-i-use-a-custom-clip-model) it is mandatory to re-run the Smart Search job on all images to fully apply the change. diff --git a/docs/docs/features/facial-recognition.md b/docs/docs/features/facial-recognition.md index 32ca6c87ba..f0dec55484 100644 --- a/docs/docs/features/facial-recognition.md +++ b/docs/docs/features/facial-recognition.md @@ -69,6 +69,8 @@ Navigating to Administration > Settings > Machine Learning Settings > Facial Rec :::tip It's better to only tweak the parameters here than to set them to something very different unless you're ready to test a variety of options. If you do need to set a parameter to a strict setting, relaxing other settings can be a good option to compensate, and vice versa. + +You can learn how the tune the result in this [Guide](/docs/guides/better-facial-clusters) ::: ### Facial recognition model diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index a137980e00..3d4ab6a892 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -68,7 +68,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up. ### Nightly job -There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. +There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library managment page. ## Usage diff --git a/docs/docs/guides/better-facial-clusters.md b/docs/docs/guides/better-facial-clusters.md new file mode 100644 index 0000000000..f4409b441c --- /dev/null +++ b/docs/docs/guides/better-facial-clusters.md @@ -0,0 +1,72 @@ +# Better Facial Recognition Clusters + +## Purpose + +This guide explains how to optimize facial recognition in systems with large image libraries. By following these steps, you'll achieve better clustering of faces, reducing the need for manual merging. + +--- + +## Important Notes + +- **Best Suited For:** Large image libraries after importing a significant number of images. +- **Warning:** This method deletes all previously assigned names. +- **Tip:** **Always take a [backup](/docs/administration/backup-and-restore#database) before proceeding!** + +--- + +## Step-by-Step Instructions + +### Objective + +To enhance face clustering and ensure the model effectively identifies faces using qualitative initial data. + +--- + +### Steps + +#### 1. Adjust Machine Learning Settings + +Navigate to: +**Admin → Administration → Settings → Machine Learning Settings** + +Make the following changes: + +- **Maximum recognition distance (Optional):** + Lower this value, e.g., to **0.4**, if the library contains people with similar facial features. +- **Minimum recognized faces:** + Set this to a **high value** (e.g., 20 For libraries with a large amount of assets (~100K+), and 10 for libraries with medium amount of assets (~40K+)). + > A high value ensures clusters only include faces that appear at least 20/`value` times in the library, improving the initial clustering process. + +--- + +#### 2. Run Reset Jobs + +Go to: +**Admin → Administration → Settings → Jobs** + +Perform the following: + +1. **FACIAL RECOGNITION → Reset** + +> These reset jobs rebuild the recognition model based on the new settings. + +--- + +#### 3. Refine Recognition with Lower Thresholds + +Once the reset jobs are complete, refine the recognition as follows: + +- **Step 1:** + Return to **Minimum recognized faces** in Machine Learning Settings and lower the value to **10** (In medium libraries we will lower the value from 10 to 5). + + > Run the job: **FACIAL RECOGNITION → MISSING Mode** + +- **Step 2:** + Lower the value again to **3**. + > Run the job: **FACIAL RECOGNITION → MISSING Mode** + +:::tip try different values +For certain libraries with a larger or smaller amount of assets, other settings will be better or worse. It is recommended to try different values **​​before assigning names** and see which settings work best for your library. +::: + +--- diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index 2017689984..89a4f07bc0 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -31,6 +31,10 @@ SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%'; SELECT * FROM "assets" WHERE "id" = '9f94e60f-65b6-47b7-ae44-a4df7b57f0e9'; ``` +```sql title="Find by partial ID" +SELECT * FROM "assets" WHERE "id"::text LIKE '%ab431d3a%'; +``` + :::note You can calculate the checksum for a particular file by using the command `sha1sum `. ::: diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx index 3593cf19ee..99a29397fa 100644 --- a/docs/docs/install/docker-compose.mdx +++ b/docs/docs/install/docker-compose.mdx @@ -37,7 +37,7 @@ You can alternatively download these two files from your browser and move them t - Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. It should be a new directory on the server with enough free space. -- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication. +- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publicly exposed, so this password is only used for local authentication. To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. `pwgen` is a handy utility for this. - Set your timezone by uncommenting the `TZ=` line. - Populate custom database information if necessary. diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index a57eef540d..8b9f74d455 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -11,7 +11,7 @@ Just restarting the containers does not replace the environment within the conta In order to recreate the container using docker compose, run `docker compose up -d`. In most cases docker will recognize that the `.env` file has changed and recreate the affected containers. -If this should not work, try running `docker compose up -d --force-recreate`. +If this does not work, try running `docker compose up -d --force-recreate`. ::: @@ -20,8 +20,8 @@ If this should not work, try running `docker compose up -d --force-recreate`. | Variable | Description | Default | Containers | | :----------------- | :------------------------------ | :-------: | :----------------------- | | `IMMICH_VERSION` | Image tags | `release` | server, machine learning | -| `UPLOAD_LOCATION` | Host Path for uploads | | server | -| `DB_DATA_LOCATION` | Host Path for Postgres database | | database | +| `UPLOAD_LOCATION` | Host path for uploads | | server | +| `DB_DATA_LOCATION` | Host path for Postgres database | | database | :::tip These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly. @@ -33,15 +33,15 @@ These environment variables are used by the `docker-compose.yml` file and do **N | :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- | | `TZ` | Timezone | \*1 | server | microservices | | `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | -| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | -| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `./upload`\*3 | server | api, microservices | +| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | +| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `./upload`\*3 | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | -| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | | +| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | | | `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api | | `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices | | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | -| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api | +| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api | | `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices | \*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. @@ -50,7 +50,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N \*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead. \*3: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`. -It only need to be set if the Immich deployment method is changing. +It only needs to be set if the Immich deployment method is changing. ## Workers @@ -75,12 +75,12 @@ Information on the current workers can be found [here](/docs/administration/jobs | Variable | Description | Default | Containers | | :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- | | `DB_URL` | Database URL | | server | -| `DB_HOSTNAME` | Database Host | `database` | server | -| `DB_PORT` | Database Port | `5432` | server | -| `DB_USERNAME` | Database User | `postgres` | server, database\*1 | -| `DB_PASSWORD` | Database Password | `postgres` | server, database\*1 | -| `DB_DATABASE_NAME` | Database Name | `immich` | server, database\*1 | -| `DB_VECTOR_EXTENSION`\*2 | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server | +| `DB_HOSTNAME` | Database host | `database` | server | +| `DB_PORT` | Database port | `5432` | server | +| `DB_USERNAME` | Database user | `postgres` | server, database\*1 | +| `DB_PASSWORD` | Database password | `postgres` | server, database\*1 | +| `DB_DATABASE_NAME` | Database name | `immich` | server, database\*1 | +| `DB_VECTOR_EXTENSION`\*2 | Database vector extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server | | `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server | \*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`. @@ -103,18 +103,18 @@ When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSW | Variable | Description | Default | Containers | | :--------------- | :------------- | :-----: | :--------- | | `REDIS_URL` | Redis URL | | server | -| `REDIS_SOCKET` | Redis Socket | | server | -| `REDIS_HOSTNAME` | Redis Host | `redis` | server | -| `REDIS_PORT` | Redis Port | `6379` | server | -| `REDIS_USERNAME` | Redis Username | | server | -| `REDIS_PASSWORD` | Redis Password | | server | -| `REDIS_DBINDEX` | Redis DB Index | `0` | server | +| `REDIS_SOCKET` | Redis socket | | server | +| `REDIS_HOSTNAME` | Redis host | `redis` | server | +| `REDIS_PORT` | Redis port | `6379` | server | +| `REDIS_USERNAME` | Redis username | | server | +| `REDIS_PASSWORD` | Redis password | | server | +| `REDIS_DBINDEX` | Redis DB index | `0` | server | :::info All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`. `REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration. -More info can be found in the upstream [ioredis] documentation. +More information can be found in the upstream [ioredis] documentation. When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored. ::: @@ -168,6 +168,8 @@ Redis (Sentinel) URL example JSON before encoding: | `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | | `MACHINE_LEARNING_DEVICE_IDS`\*4 | Device IDs to use in multi-GPU environments | `0` | machine learning | | `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning | +| `MACHINE_LEARNING_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server | +| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server | \*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones. @@ -179,7 +181,11 @@ Redis (Sentinel) URL example JSON before encoding: :::info -Other machine learning parameters can be tuned from the admin UI. +While the `textual` model is the only one required for smart search, some users may experience slow first searches +due to backups triggering loading of the other models into memory, which blocks other requests until completed. +To avoid this, you can preload the other models (`visual`, `recognition`, and `detection`) if you have enough RAM to do so. + +Additional machine learning parameters can be tuned from the admin UI. ::: @@ -210,7 +216,7 @@ the `_FILE` variable should be set to the path of a file containing the variable details on how to use Docker Secrets in the Postgres image. \*2: See [this comment][docker-secrets-example] for an example of how -to use use a Docker secret for the password in the Redis container. +to use a Docker secret for the password in the Redis container. [tz-list]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List [docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234 diff --git a/docs/docs/install/truenas.md b/docs/docs/install/truenas.md index 049af1250e..31b007a47d 100644 --- a/docs/docs/install/truenas.md +++ b/docs/docs/install/truenas.md @@ -198,7 +198,7 @@ The **CPU** value was specified in a different format with a default of `4000m` The **Memory** value was specified in a different format with a default of `8Gi` which is 8 GiB of RAM. The value was specified in bytes or a number with a measurement suffix. Examples: `129M`, `123Mi`, `1000000000` ::: -Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passtrough Docs for TrueNAS Apps](https://www.truenas.com/docs/truenasapps/#gpu-passthrough) +Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passthrough Docs for TrueNAS Apps](https://www.truenas.com/docs/truenasapps/#gpu-passthrough) ### Install diff --git a/docs/package-lock.json b/docs/package-lock.json index 52208c0672..2161fca83b 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -14070,9 +14070,9 @@ } }, "node_modules/postcss": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", - "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -15734,9 +15734,9 @@ } }, "node_modules/prettier": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", - "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", + "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", "dev": true, "license": "MIT", "bin": { diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx index 46997fdfb7..b7ded8e8c9 100644 --- a/docs/src/pages/roadmap.tsx +++ b/docs/src/pages/roadmap.tsx @@ -242,6 +242,13 @@ const roadmap: Item[] = [ ]; const milestones: Item[] = [ + { + icon: mdiStar, + iconColor: 'gold', + title: '60,000 Stars', + description: 'Reached 60K Stars on GitHub!', + getDateLabel: withLanguage(new Date(2025, 2, 4)), + }, withRelease({ icon: mdiLinkEdit, iconColor: 'crimson', diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 516a978f50..b476c8f8de 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,16 @@ [ + { + "label": "v1.129.0", + "url": "https://v1.129.0.archive.immich.app" + }, + { + "label": "v1.128.0", + "url": "https://v1.128.0.archive.immich.app" + }, + { + "label": "v1.127.0", + "url": "https://v1.127.0.archive.immich.app" + }, { "label": "v1.126.1", "url": "https://v1.126.1.archive.immich.app" diff --git a/docs/tailwind.config.js b/docs/tailwind.config.js index 98f69bcd59..5ed28c737d 100644 --- a/docs/tailwind.config.js +++ b/docs/tailwind.config.js @@ -5,7 +5,7 @@ module.exports = { preflight: false, // disable Tailwind's reset }, content: ['./src/**/*.{js,jsx,ts,tsx}', './{docs,blog}/**/*.{md,mdx}'], // my markdown stuff is in ../docs, not /src - darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns + darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settings theme: { extend: { colors: { diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index d6f110cbfa..e05d7734ed 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -37,7 +37,7 @@ services: image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8 database: - image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 + image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52 command: -c fsync=off -c shared_preload_libraries=vectors.so environment: POSTGRES_PASSWORD: postgres diff --git a/e2e/package-lock.json b/e2e/package-lock.json index c32beef97f..2ed6990817 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.126.1", + "version": "1.129.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.126.1", + "version": "1.129.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -15,7 +15,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -28,7 +28,7 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^56.0.1", "exiftool-vendored": "^28.3.1", - "globals": "^15.9.0", + "globals": "^16.0.0", "jose": "^5.6.3", "luxon": "^3.4.4", "oidc-provider": "^8.5.1", @@ -45,13 +45,15 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.50", + "version": "2.2.53", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { + "chokidar": "^4.0.3", "fast-glob": "^3.3.2", "fastq": "^1.17.1", - "lodash-es": "^4.17.21" + "lodash-es": "^4.17.21", + "micromatch": "^4.0.8" }, "bin": { "immich": "dist/index.js" @@ -63,8 +65,9 @@ "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", + "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "@vitest/coverage-v8": "^3.0.0", @@ -75,7 +78,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^56.0.1", - "globals": "^15.9.0", + "globals": "^16.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", @@ -92,14 +95,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.126.1", + "version": "1.129.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "typescript": "^5.3.3" } }, @@ -361,9 +364,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", "cpu": [ "ppc64" ], @@ -378,9 +381,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", "cpu": [ "arm" ], @@ -395,9 +398,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", "cpu": [ "arm64" ], @@ -412,9 +415,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", "cpu": [ "x64" ], @@ -429,9 +432,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", "cpu": [ "arm64" ], @@ -446,9 +449,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", "cpu": [ "x64" ], @@ -463,9 +466,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", "cpu": [ "arm64" ], @@ -480,9 +483,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", "cpu": [ "x64" ], @@ -497,9 +500,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", "cpu": [ "arm" ], @@ -514,9 +517,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", "cpu": [ "arm64" ], @@ -531,9 +534,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", "cpu": [ "ia32" ], @@ -548,9 +551,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", "cpu": [ "loong64" ], @@ -565,9 +568,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", "cpu": [ "mips64el" ], @@ -582,9 +585,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", "cpu": [ "ppc64" ], @@ -599,9 +602,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", "cpu": [ "riscv64" ], @@ -616,9 +619,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", "cpu": [ "s390x" ], @@ -633,9 +636,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", "cpu": [ "x64" ], @@ -650,9 +653,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", "cpu": [ "arm64" ], @@ -667,9 +670,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", "cpu": [ "x64" ], @@ -684,9 +687,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", "cpu": [ "arm64" ], @@ -701,9 +704,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", "cpu": [ "x64" ], @@ -718,9 +721,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", "cpu": [ "x64" ], @@ -735,9 +738,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", "cpu": [ "arm64" ], @@ -752,9 +755,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", "cpu": [ "ia32" ], @@ -769,9 +772,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", "cpu": [ "x64" ], @@ -811,13 +814,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -826,9 +829,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -839,9 +842,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -876,9 +879,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", "dev": true, "license": "MIT", "engines": { @@ -886,9 +889,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -896,13 +899,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.12.0", "levn": "^0.4.1" }, "engines": { @@ -961,9 +964,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1261,9 +1264,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz", - "integrity": "sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz", + "integrity": "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==", "cpu": [ "arm" ], @@ -1275,9 +1278,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.6.tgz", - "integrity": "sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.9.tgz", + "integrity": "sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==", "cpu": [ "arm64" ], @@ -1289,9 +1292,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.6.tgz", - "integrity": "sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.9.tgz", + "integrity": "sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==", "cpu": [ "arm64" ], @@ -1303,9 +1306,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.6.tgz", - "integrity": "sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.9.tgz", + "integrity": "sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==", "cpu": [ "x64" ], @@ -1317,9 +1320,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.6.tgz", - "integrity": "sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.9.tgz", + "integrity": "sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==", "cpu": [ "arm64" ], @@ -1331,9 +1334,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.6.tgz", - "integrity": "sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.9.tgz", + "integrity": "sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==", "cpu": [ "x64" ], @@ -1345,9 +1348,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.6.tgz", - "integrity": "sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.9.tgz", + "integrity": "sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==", "cpu": [ "arm" ], @@ -1359,9 +1362,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.6.tgz", - "integrity": "sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.9.tgz", + "integrity": "sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==", "cpu": [ "arm" ], @@ -1373,9 +1376,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.6.tgz", - "integrity": "sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.9.tgz", + "integrity": "sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==", "cpu": [ "arm64" ], @@ -1387,9 +1390,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.6.tgz", - "integrity": "sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.9.tgz", + "integrity": "sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==", "cpu": [ "arm64" ], @@ -1401,9 +1404,9 @@ ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.6.tgz", - "integrity": "sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.9.tgz", + "integrity": "sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==", "cpu": [ "loong64" ], @@ -1415,9 +1418,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.6.tgz", - "integrity": "sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.9.tgz", + "integrity": "sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==", "cpu": [ "ppc64" ], @@ -1429,9 +1432,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.6.tgz", - "integrity": "sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.9.tgz", + "integrity": "sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==", "cpu": [ "riscv64" ], @@ -1443,9 +1446,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.6.tgz", - "integrity": "sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.9.tgz", + "integrity": "sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==", "cpu": [ "s390x" ], @@ -1457,9 +1460,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.6.tgz", - "integrity": "sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz", + "integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==", "cpu": [ "x64" ], @@ -1471,9 +1474,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.6.tgz", - "integrity": "sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.9.tgz", + "integrity": "sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==", "cpu": [ "x64" ], @@ -1485,9 +1488,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.6.tgz", - "integrity": "sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.9.tgz", + "integrity": "sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==", "cpu": [ "arm64" ], @@ -1499,9 +1502,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.6.tgz", - "integrity": "sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.9.tgz", + "integrity": "sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==", "cpu": [ "ia32" ], @@ -1513,9 +1516,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.6.tgz", - "integrity": "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.9.tgz", + "integrity": "sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==", "cpu": [ "x64" ], @@ -1714,9 +1717,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", - "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "version": "22.13.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", + "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1730,12 +1733,13 @@ "dev": true }, "node_modules/@types/oidc-provider": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.5.2.tgz", - "integrity": "sha512-NiD3VG49+cRCAAe8+uZLM4onOcX8y9+cwaml8JG1qlgc98rWoCRgsnOB4Ypx+ysays5jiwzfUgT0nWyXPB/9uQ==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.8.0.tgz", + "integrity": "sha512-9Jtutw4dyAz0PN8EWlxqeNrGHsEZ9EH4QZjfkZIbhmKuiuHIrzoz/S1zHNXX8ogfhWtPp1swMyzXBSo6RTTj1Q==", "dev": true, "license": "MIT", "dependencies": { + "@types/keygrip": "*", "@types/koa": "*", "@types/node": "*" } @@ -1878,17 +1882,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", - "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz", + "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/type-utils": "8.24.0", - "@typescript-eslint/utils": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/type-utils": "8.25.0", + "@typescript-eslint/utils": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1908,16 +1912,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", - "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz", + "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/typescript-estree": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "engines": { @@ -1933,14 +1937,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", - "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz", + "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0" + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1951,14 +1955,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", - "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz", + "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/utils": "8.24.0", + "@typescript-eslint/typescript-estree": "8.25.0", + "@typescript-eslint/utils": "8.25.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -1975,9 +1979,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", - "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", + "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", "dev": true, "license": "MIT", "engines": { @@ -1989,14 +1993,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", - "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz", + "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2042,16 +2046,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", - "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz", + "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0" + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/typescript-estree": "8.25.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2066,13 +2070,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", - "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz", + "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2097,9 +2101,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz", - "integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.7.tgz", + "integrity": "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2120,8 +2124,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.5", - "vitest": "3.0.5" + "@vitest/browser": "3.0.7", + "vitest": "3.0.7" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2148,15 +2152,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", - "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, "funding": { @@ -2164,13 +2168,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", - "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", + "@vitest/spy": "3.0.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -2191,9 +2195,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", - "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", "dev": true, "license": "MIT", "dependencies": { @@ -2204,38 +2208,38 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", - "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.5", - "pathe": "^2.0.2" + "@vitest/utils": "3.0.7", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", - "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", + "@vitest/pretty-format": "3.0.7", "magic-string": "^0.30.17", - "pathe": "^2.0.2" + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", - "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", "dev": true, "license": "MIT", "dependencies": { @@ -2246,14 +2250,14 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", - "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", - "loupe": "^3.1.2", + "@vitest/pretty-format": "3.0.7", + "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, "funding": { @@ -2602,9 +2606,9 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", "dependencies": { @@ -3084,9 +3088,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3097,31 +3101,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" } }, "node_modules/escalade": { @@ -3153,22 +3157,22 @@ } }, "node_modules/eslint": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", - "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", + "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.19.2", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.21.0", + "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -3213,9 +3217,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", - "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.2.tgz", + "integrity": "sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==", "dev": true, "license": "MIT", "bin": { @@ -3290,6 +3294,19 @@ "eslint": ">=8.56.0" } }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", @@ -3319,19 +3336,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", @@ -3554,9 +3558,9 @@ "dev": true }, "node_modules/fastq": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.0.tgz", - "integrity": "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3819,9 +3823,9 @@ } }, "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", "dev": true, "license": "MIT", "engines": { @@ -4333,9 +4337,9 @@ } }, "node_modules/jose": { - "version": "5.9.6", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", - "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", "dev": true, "license": "MIT", "funding": { @@ -4934,9 +4938,9 @@ "dev": true }, "node_modules/oidc-provider": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.7.0.tgz", - "integrity": "sha512-H0AE07n7d5zBHwP8bDb1Yg+NokP+BybisUPB2kGG/lI0aPvLL/JcEhh3vJsz3UbThoz2p+6a0xGTBA8a3yDUGg==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.8.0.tgz", + "integrity": "sha512-5b4QncVOVsU8BLpD0ofQBRq2aX9Juhc0wFbaZSQbAmgN1jVfCZfYt3GEPPmJ8Tc/mvfX735PNH/LnuyWzMn9tQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5190,9 +5194,9 @@ "dev": true }, "node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -5379,9 +5383,9 @@ } }, "node_modules/postcss": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", - "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "dev": true, "funding": [ { @@ -5481,9 +5485,9 @@ } }, "node_modules/prettier": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", - "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", + "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", "dev": true, "license": "MIT", "bin": { @@ -5792,9 +5796,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -5818,9 +5822,9 @@ } }, "node_modules/rollup": { - "version": "4.34.6", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.6.tgz", - "integrity": "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==", + "version": "4.34.9", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.9.tgz", + "integrity": "sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5834,25 +5838,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.34.6", - "@rollup/rollup-android-arm64": "4.34.6", - "@rollup/rollup-darwin-arm64": "4.34.6", - "@rollup/rollup-darwin-x64": "4.34.6", - "@rollup/rollup-freebsd-arm64": "4.34.6", - "@rollup/rollup-freebsd-x64": "4.34.6", - "@rollup/rollup-linux-arm-gnueabihf": "4.34.6", - "@rollup/rollup-linux-arm-musleabihf": "4.34.6", - "@rollup/rollup-linux-arm64-gnu": "4.34.6", - "@rollup/rollup-linux-arm64-musl": "4.34.6", - "@rollup/rollup-linux-loongarch64-gnu": "4.34.6", - "@rollup/rollup-linux-powerpc64le-gnu": "4.34.6", - "@rollup/rollup-linux-riscv64-gnu": "4.34.6", - "@rollup/rollup-linux-s390x-gnu": "4.34.6", - "@rollup/rollup-linux-x64-gnu": "4.34.6", - "@rollup/rollup-linux-x64-musl": "4.34.6", - "@rollup/rollup-win32-arm64-msvc": "4.34.6", - "@rollup/rollup-win32-ia32-msvc": "4.34.6", - "@rollup/rollup-win32-x64-msvc": "4.34.6", + "@rollup/rollup-android-arm-eabi": "4.34.9", + "@rollup/rollup-android-arm64": "4.34.9", + "@rollup/rollup-darwin-arm64": "4.34.9", + "@rollup/rollup-darwin-x64": "4.34.9", + "@rollup/rollup-freebsd-arm64": "4.34.9", + "@rollup/rollup-freebsd-x64": "4.34.9", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.9", + "@rollup/rollup-linux-arm-musleabihf": "4.34.9", + "@rollup/rollup-linux-arm64-gnu": "4.34.9", + "@rollup/rollup-linux-arm64-musl": "4.34.9", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.9", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.9", + "@rollup/rollup-linux-riscv64-gnu": "4.34.9", + "@rollup/rollup-linux-s390x-gnu": "4.34.9", + "@rollup/rollup-linux-x64-gnu": "4.34.9", + "@rollup/rollup-linux-x64-musl": "4.34.9", + "@rollup/rollup-win32-arm64-msvc": "4.34.9", + "@rollup/rollup-win32-ia32-msvc": "4.34.9", + "@rollup/rollup-win32-x64-msvc": "4.34.9", "fsevents": "~2.3.2" } }, @@ -6589,14 +6593,14 @@ } }, "node_modules/vite": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", - "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.5.1", + "esbuild": "^0.25.0", + "postcss": "^8.5.3", "rollup": "^4.30.1" }, "bin": { @@ -6661,16 +6665,16 @@ } }, "node_modules/vite-node": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", - "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", - "pathe": "^2.0.2", + "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { @@ -6717,31 +6721,31 @@ } }, "node_modules/vitest": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", - "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.5", - "@vitest/mocker": "3.0.5", - "@vitest/pretty-format": "^3.0.5", - "@vitest/runner": "3.0.5", - "@vitest/snapshot": "3.0.5", - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", - "pathe": "^2.0.2", + "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.5", + "vite-node": "3.0.7", "why-is-node-running": "^2.3.0" }, "bin": { @@ -6757,8 +6761,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.5", - "@vitest/ui": "3.0.5", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", "happy-dom": "*", "jsdom": "*" }, diff --git a/e2e/package.json b/e2e/package.json index c0d9ec424e..657a51f568 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.126.1", + "version": "1.129.0", "description": "", "main": "index.js", "type": "module", @@ -25,7 +25,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -38,7 +38,7 @@ "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^56.0.1", "exiftool-vendored": "^28.3.1", - "globals": "^15.9.0", + "globals": "^16.0.0", "jose": "^5.6.3", "luxon": "^3.4.4", "oidc-provider": "^8.5.1", diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 1b644454aa..8700356256 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -4,7 +4,6 @@ import { AssetResponseDto, AssetTypeEnum, getAssetInfo, - getConfig, getMyUser, LoginResponseDto, SharedLinkType, @@ -45,8 +44,6 @@ const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-sp const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`; const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`; -const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) }); - const readTags = async (bytes: Buffer, filename: string) => { const filepath = join(tempDir, filename); await writeFile(filepath, bytes); @@ -228,7 +225,7 @@ describe('/asset', () => { }); it('should get the asset faces', async () => { - const config = await getSystemConfig(admin.accessToken); + const config = await utils.getSystemConfig(admin.accessToken); config.metadata.faces.import = true; await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) }); diff --git a/e2e/src/api/specs/jobs.e2e-spec.ts b/e2e/src/api/specs/jobs.e2e-spec.ts index 4b8126c941..a9afd8475f 100644 --- a/e2e/src/api/specs/jobs.e2e-spec.ts +++ b/e2e/src/api/specs/jobs.e2e-spec.ts @@ -1,8 +1,9 @@ -import { JobCommand, JobName, LoginResponseDto } from '@immich/sdk'; +import { JobCommand, JobName, LoginResponseDto, updateConfig } from '@immich/sdk'; +import { cpSync, rmSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import { basename } from 'node:path'; import { errorDto } from 'src/responses'; -import { app, testAssetDir, utils } from 'src/utils'; +import { app, asBearerAuth, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterEach, beforeAll, describe, expect, it } from 'vitest'; @@ -20,6 +21,33 @@ describe('/jobs', () => { command: JobCommand.Resume, force: false, }); + + await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, { + command: JobCommand.Resume, + force: false, + }); + + await utils.jobCommand(admin.accessToken, JobName.FaceDetection, { + command: JobCommand.Resume, + force: false, + }); + + await utils.jobCommand(admin.accessToken, JobName.SmartSearch, { + command: JobCommand.Resume, + force: false, + }); + + await utils.jobCommand(admin.accessToken, JobName.DuplicateDetection, { + command: JobCommand.Resume, + force: false, + }); + + const config = await utils.getSystemConfig(admin.accessToken); + config.machineLearning.duplicateDetection.enabled = false; + config.machineLearning.enabled = false; + config.metadata.faces.import = false; + config.machineLearning.clip.enabled = false; + await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) }); }); it('should require authentication', async () => { @@ -29,14 +57,7 @@ describe('/jobs', () => { }); it('should queue metadata extraction for missing assets', async () => { - const path1 = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`; - const path2 = `${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`; - - await utils.createAsset(admin.accessToken, { - assetData: { bytes: await readFile(path1), filename: basename(path1) }, - }); - - await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + const path = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`; await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, { command: JobCommand.Pause, @@ -44,7 +65,7 @@ describe('/jobs', () => { }); const { id } = await utils.createAsset(admin.accessToken, { - assetData: { bytes: await readFile(path2), filename: basename(path2) }, + assetData: { bytes: await readFile(path), filename: basename(path) }, }); await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); @@ -82,5 +103,123 @@ describe('/jobs', () => { expect(asset.exifInfo?.make).toBe('NIKON CORPORATION'); } }); + + it('should not re-extract metadata for existing assets', async () => { + const path = `${testAssetDir}/temp/metadata/asset.jpg`; + + cpSync(`${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`, path); + + const { id } = await utils.createAsset(admin.accessToken, { + assetData: { bytes: await readFile(path), filename: basename(path) }, + }); + + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + { + const asset = await utils.getAssetInfo(admin.accessToken, id); + + expect(asset.exifInfo).toBeDefined(); + expect(asset.exifInfo?.model).toBe('NIKON D700'); + } + + cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, path); + + await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, { + command: JobCommand.Start, + force: false, + }); + + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + + { + const asset = await utils.getAssetInfo(admin.accessToken, id); + + expect(asset.exifInfo).toBeDefined(); + expect(asset.exifInfo?.model).toBe('NIKON D700'); + } + + rmSync(path); + }); + + it('should queue thumbnail extraction for assets missing thumbs', async () => { + const path = `${testAssetDir}/albums/nature/tanners_ridge.jpg`; + + await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, { + command: JobCommand.Pause, + force: false, + }); + + const { id } = await utils.createAsset(admin.accessToken, { + assetData: { bytes: await readFile(path), filename: basename(path) }, + }); + + await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction); + await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration); + + const assetBefore = await utils.getAssetInfo(admin.accessToken, id); + expect(assetBefore.thumbhash).toBeNull(); + + await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, { + command: JobCommand.Empty, + force: false, + }); + + await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction); + await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration); + + await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, { + command: JobCommand.Resume, + force: false, + }); + + await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, { + command: JobCommand.Start, + force: false, + }); + + await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction); + await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration); + + const assetAfter = await utils.getAssetInfo(admin.accessToken, id); + expect(assetAfter.thumbhash).not.toBeNull(); + }); + + it('should not reload existing thumbnail when running thumb job for missing assets', async () => { + const path = `${testAssetDir}/temp/thumbs/asset1.jpg`; + + cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, path); + + const { id } = await utils.createAsset(admin.accessToken, { + assetData: { bytes: await readFile(path), filename: basename(path) }, + }); + + await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction); + await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration); + + const assetBefore = await utils.getAssetInfo(admin.accessToken, id); + + cpSync(`${testAssetDir}/albums/nature/notocactus_minimus.jpg`, path); + + await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, { + command: JobCommand.Resume, + force: false, + }); + + // This runs the missing thumbnail job + await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, { + command: JobCommand.Start, + force: false, + }); + + await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction); + await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration); + + const assetAfter = await utils.getAssetInfo(admin.accessToken, id); + + // Asset 1 thumbnail should be untouched since its thumb should not have been reloaded, even though the file was changed + expect(assetAfter.thumbhash).toEqual(assetBefore.thumbhash); + + rmSync(path); + }); }); }); diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 19160fec88..4b340a2da5 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -526,6 +526,47 @@ describe('/libraries', () => { utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`); }); + it('should not reimport a modified file more than once', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/reimport`], + }); + + utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`); + await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000); + + await utils.scan(admin.accessToken, library.id); + + cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`); + await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001); + + await utils.scan(admin.accessToken, library.id); + + cpSync(`${testAssetDir}/albums/nature/el_torcal_rocks.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`); + await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001); + + await utils.scan(admin.accessToken, library.id); + + const { assets } = await utils.searchAssets(admin.accessToken, { + libraryId: library.id, + }); + + expect(assets.count).toEqual(1); + + const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id); + + expect(asset).toEqual( + expect.objectContaining({ + originalFileName: 'asset.jpg', + exifInfo: expect.objectContaining({ + model: 'NIKON D750', + }), + }), + ); + + utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`); + }); + it('should set an asset offline if its file is missing', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts index 15b915ef2a..7a1a61f946 100644 --- a/e2e/src/api/specs/trash.e2e-spec.ts +++ b/e2e/src/api/specs/trash.e2e-spec.ts @@ -1,4 +1,4 @@ -import { LoginResponseDto, getAssetInfo, getAssetStatistics, scanLibrary } from '@immich/sdk'; +import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk'; import { existsSync } from 'node:fs'; import { Socket } from 'socket.io-client'; import { errorDto } from 'src/responses'; @@ -6,8 +6,6 @@ import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'sr import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) }); - describe('/trash', () => { let admin: LoginResponseDto; let ws: Socket; @@ -81,8 +79,7 @@ describe('/trash', () => { utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.scan(admin.accessToken, library.id); const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); expect(assets.items.length).toBe(1); @@ -90,8 +87,7 @@ describe('/trash', () => { await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] }); - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.scan(admin.accessToken, library.id); const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id); expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true }); @@ -116,8 +112,7 @@ describe('/trash', () => { utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.scan(admin.accessToken, library.id); const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); expect(assets.items.length).toBe(1); @@ -125,8 +120,7 @@ describe('/trash', () => { await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] }); - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.scan(admin.accessToken, library.id); const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id); expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true }); @@ -180,8 +174,7 @@ describe('/trash', () => { utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.scan(admin.accessToken, library.id); const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); expect(assets.count).toBe(1); @@ -189,9 +182,7 @@ describe('/trash', () => { await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] }); - await scan(admin.accessToken, library.id); - - await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.scan(admin.accessToken, library.id); const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true })); @@ -201,6 +192,8 @@ describe('/trash', () => { const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true })); + + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); }); }); @@ -238,7 +231,7 @@ describe('/trash', () => { utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); - await scan(admin.accessToken, library.id); + await utils.scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id }); @@ -247,7 +240,7 @@ describe('/trash', () => { await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] }); - await scan(admin.accessToken, library.id); + await utils.scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); const before = await utils.getAssetInfo(admin.accessToken, assetId); @@ -261,6 +254,8 @@ describe('/trash', () => { const after = await utils.getAssetInfo(admin.accessToken, assetId); expect(after.isTrashed).toBe(true); + + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); }); }); }); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index ecd002a8b9..7446bb708f 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -28,6 +28,7 @@ import { deleteAssets, getAllJobsStatus, getAssetInfo, + getConfig, getConfigDefaults, login, scanLibrary, @@ -121,6 +122,7 @@ const execPromise = promisify(exec); const onEvent = ({ event, id }: { event: EventType; id: string }) => { // console.log(`Received event: ${event} [id=${id}]`); const set = events[event]; + set.add(id); const idCallback = idCallbacks[id]; @@ -415,6 +417,8 @@ export const utils = { rmSync(path, { recursive: true }); }, + getSystemConfig: (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) }), + getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) => diff --git a/i18n/en.json b/i18n/en.json index 1bf118976e..b2923c8942 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -96,7 +96,7 @@ "library_scanning_enable_description": "Enable periodic library scanning", "library_settings": "External Library", "library_settings_description": "Manage external library settings", - "library_tasks_description": "Perform library tasks", + "library_tasks_description": "Scan external libraries for new and/or changed assets", "library_watching_enable_description": "Watch external libraries for file changes", "library_watching_settings": "Library watching (EXPERIMENTAL)", "library_watching_settings_description": "Automatically watch for changed files", @@ -131,7 +131,7 @@ "machine_learning_smart_search_description": "Search for images semantically using CLIP embeddings", "machine_learning_smart_search_enabled": "Enable smart search", "machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.", - "machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last.", + "machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.", "manage_concurrency": "Manage Concurrency", "manage_log_settings": "Manage log settings", "map_dark_style": "Dark style", @@ -336,6 +336,7 @@ "untracked_files": "Untracked Files", "untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", "user_cleanup_job": "User cleanup", + "cleanup": "Cleanup", "user_delete_delay": "{user}'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.", "user_delete_delay_settings": "Delete delay", "user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.", @@ -393,6 +394,7 @@ "allow_edits": "Allow edits", "allow_public_user_to_download": "Allow public user to download", "allow_public_user_to_upload": "Allow public user to upload", + "alt_text_qr_code": "QR code image", "anti_clockwise": "Anti-clockwise", "api_key": "API Key", "api_key_description": "This value will only be shown once. Please be sure to copy it before closing the window.", @@ -889,6 +891,7 @@ "month": "Month", "more": "More", "moved_to_trash": "Moved to trash", + "mute_memories": "Mute Memories", "my_albums": "My albums", "name": "Name", "name_or_nickname": "Name or nickname", @@ -1114,6 +1117,7 @@ "say_something": "Say something", "scan_all_libraries": "Scan All Libraries", "scan_library": "Scan", + "rescan": "Rescan", "scan_settings": "Scan Settings", "scanning_for_album": "Scanning for album...", "search": "Search", @@ -1302,6 +1306,7 @@ "unnamed_album": "Unnamed Album", "unnamed_album_delete_confirmation": "Are you sure you want to delete this album?", "unnamed_share": "Unnamed Share", + "unmute_memories": "Unmute Memories", "unsaved_change": "Unsaved change", "unselect_all": "Unselect all", "unselect_all_duplicates": "Unselect all duplicates", @@ -1352,6 +1357,7 @@ "view_all": "View All", "view_all_users": "View all users", "view_in_timeline": "View in timeline", + "view_link": "View link", "view_links": "View links", "view_name": "View", "view_next_asset": "View next asset", diff --git a/localizely.yml b/localizely.yml index 6da9423b49..ad3d9b26f3 100644 --- a/localizely.yml +++ b/localizely.yml @@ -66,8 +66,8 @@ download: locale_code: es-MX - file: mobile/assets/i18n/sv-FI.json locale_code: sv-FI - - file: mobile/assets/i18n/ca-CA.json - locale_code: ca-CA + - file: mobile/assets/i18n/ca.json + locale_code: ca - file: mobile/assets/i18n/hu-HU.json locale_code: hu-HU - file: mobile/assets/i18n/lv-LV.json diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index d888731149..8761586de7 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:14b4620f59a90f163dfa6bd252b68743f9a41d494a9fde935f9d7669d98094bb AS builder-cpu +FROM python:3.11-bookworm@sha256:68a8863d0625f42d47e0684f33ca02f19d6094ef859a8af237aaf645195ed477 AS builder-cpu FROM builder-cpu AS builder-openvino @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:42420f737ba91d509fc60d5ed65ed0492678a90c561e1fa08786ae8ba8b52eda AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:614c8691ab74150465ec9123378cd4dde7a6e57be9e558c3108df40664667a4c AS prod-cpu FROM prod-cpu AS prod-openvino diff --git a/machine-learning/app/models/facial_recognition/recognition.py b/machine-learning/app/models/facial_recognition/recognition.py index 044f19b06f..5e8a6f69ec 100644 --- a/machine-learning/app/models/facial_recognition/recognition.py +++ b/machine-learning/app/models/facial_recognition/recognition.py @@ -20,9 +20,8 @@ class FaceRecognizer(InferenceModel): depends = [(ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)] identity = (ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION) - def __init__(self, model_name: str, min_score: float = 0.7, **model_kwargs: Any) -> None: + def __init__(self, model_name: str, **model_kwargs: Any) -> None: super().__init__(model_name, **model_kwargs) - self.min_score = model_kwargs.pop("minScore", min_score) max_batch_size = settings.max_batch_size.facial_recognition if settings.max_batch_size else None self.batch_size = max_batch_size if max_batch_size else self._batch_size_default diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py index b986f63668..2d489025d7 100644 --- a/machine-learning/app/test_main.py +++ b/machine-learning/app/test_main.py @@ -324,7 +324,7 @@ class TestAnnSession: session.run(None, input_feed) ann_session.return_value.execute.assert_called_once_with(123, [input1, input2]) - np_spy.call_count == 2 + assert np_spy.call_count == 2 np_spy.assert_has_calls([mock.call(input1), mock.call(input2)]) @@ -457,11 +457,14 @@ class TestCLIP: class TestFaceRecognition: - def test_set_min_score(self, mocker: MockerFixture) -> None: - mocker.patch.object(FaceRecognizer, "load") - face_recognizer = FaceRecognizer("buffalo_s", cache_dir="test_cache", min_score=0.5) + def test_set_min_score(self, snapshot_download: mock.Mock, ort_session: mock.Mock, path: mock.Mock) -> None: + path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx" - assert face_recognizer.min_score == 0.5 + face_detector = FaceDetector("buffalo_s", min_score=0.5, cache_dir="test_cache") + face_detector.load() + + assert face_detector.min_score == 0.5 + assert face_detector.model.det_thresh == 0.5 def test_detection(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None: mocker.patch.object(FaceDetector, "load") diff --git a/machine-learning/locustfile.py b/machine-learning/locustfile.py index 81087bee8c..9a07a99688 100644 --- a/machine-learning/locustfile.py +++ b/machine-learning/locustfile.py @@ -14,12 +14,6 @@ byte_image = BytesIO() def _(parser: ArgumentParser) -> None: parser.add_argument("--clip-model", type=str, default="ViT-B-32::openai") parser.add_argument("--face-model", type=str, default="buffalo_l") - parser.add_argument( - "--tag-min-score", - type=int, - default=0.0, - help="Returns all tags at or above this score. The default returns all tags.", - ) parser.add_argument( "--face-min-score", type=int, @@ -74,10 +68,10 @@ class RecognitionFormDataLoadTest(InferenceLoadTest): "facial-recognition": { "recognition": { "modelName": self.environment.parsed_options.face_model, - "options": {"minScore": self.environment.parsed_options.face_min_score}, }, "detection": { "modelName": self.environment.parsed_options.face_model, + "options": {"minScore": self.environment.parsed_options.face_min_score}, }, } } diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 6aaf8c2972..809e1082f9 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -75,33 +75,33 @@ trio = ["trio (>=0.23)"] [[package]] name = "black" -version = "24.10.0" +version = "25.1.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" files = [ - {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, - {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, - {file = "black-24.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f"}, - {file = "black-24.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e"}, - {file = "black-24.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad"}, - {file = "black-24.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50"}, - {file = "black-24.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392"}, - {file = "black-24.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175"}, - {file = "black-24.10.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3"}, - {file = "black-24.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65"}, - {file = "black-24.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f"}, - {file = "black-24.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8"}, - {file = "black-24.10.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981"}, - {file = "black-24.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b"}, - {file = "black-24.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2"}, - {file = "black-24.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b"}, - {file = "black-24.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd"}, - {file = "black-24.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f"}, - {file = "black-24.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800"}, - {file = "black-24.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7"}, - {file = "black-24.10.0-py3-none-any.whl", hash = "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d"}, - {file = "black-24.10.0.tar.gz", hash = "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875"}, + {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, + {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, + {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, + {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, + {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, + {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, + {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, + {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, + {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, + {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, + {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, + {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, + {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, + {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, + {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, + {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, + {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, + {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, + {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, + {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, + {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, + {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, ] [package.dependencies] @@ -1331,13 +1331,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "huggingface-hub" -version = "0.28.1" +version = "0.29.1" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" files = [ - {file = "huggingface_hub-0.28.1-py3-none-any.whl", hash = "sha256:aa6b9a3ffdae939b72c464dbb0d7f99f56e649b55c3d52406f49e0a5a620c0a7"}, - {file = "huggingface_hub-0.28.1.tar.gz", hash = "sha256:893471090c98e3b6efbdfdacafe4052b20b84d59866fb6f54c33d9af18c303ae"}, + {file = "huggingface_hub-0.29.1-py3-none-any.whl", hash = "sha256:352f69caf16566c7b6de84b54a822f6238e17ddd8ae3da4f8f2272aea5b198d5"}, + {file = "huggingface_hub-0.29.1.tar.gz", hash = "sha256:9524eae42077b8ff4fc459ceb7a514eca1c1232b775276b009709fe2a084f250"}, ] [package.dependencies] @@ -1625,23 +1625,23 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.32.9" +version = "2.33.0" description = "Developer-friendly load testing framework" optional = false python-versions = ">=3.9" files = [ - {file = "locust-2.32.9-py3-none-any.whl", hash = "sha256:d9447c26d2bbaec5a0ace7cadefa1a31820ed392234257b309965a43d5e8d26f"}, - {file = "locust-2.32.9.tar.gz", hash = "sha256:4c297afa5cdc3de15dfa79279576e5f33c1d69dd70006b51d079dcbd212201cc"}, + {file = "locust-2.33.0-py3-none-any.whl", hash = "sha256:77fcc5cc35cceee5e12d99f5bb23bc441d145bdef6967c2e93d6e4d93451553e"}, + {file = "locust-2.33.0.tar.gz", hash = "sha256:ba291b7ab2349cc2db540adb8888bc93feb89ea4e4e10d80b935e5065091e8e9"}, ] [package.dependencies] -ConfigArgParse = ">=1.5.5" +configargparse = ">=1.5.5" flask = ">=2.0.0" -Flask-Cors = ">=3.0.10" -Flask-Login = ">=0.6.3" +flask-cors = ">=3.0.10" +flask-login = ">=0.6.3" gevent = [ - {version = ">=22.10.2", markers = "python_full_version <= \"3.12.0\""}, - {version = ">=24.10.1", markers = "python_full_version > \"3.13.0\""}, + {version = ">=22.10.2", markers = "python_version <= \"3.12\""}, + {version = ">=24.10.1", markers = "python_version > \"3.13\""}, ] geventhttpclient = ">=2.3.1" msgpack = ">=1.0.0" @@ -1649,13 +1649,13 @@ psutil = ">=5.9.1" pywin32 = {version = "*", markers = "sys_platform == \"win32\""} pyzmq = ">=25.0.0" requests = [ - {version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""}, - {version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""}, + {version = ">=2.26.0", markers = "python_version <= \"3.11\""}, + {version = ">=2.32.2", markers = "python_version > \"3.11\""}, ] setuptools = ">=70.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""} -Werkzeug = ">=2.0.0" +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""} +werkzeug = ">=2.0.0" [[package]] name = "markdown-it-py" @@ -2628,13 +2628,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.7.1" +version = "2.8.1" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd"}, - {file = "pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93"}, + {file = "pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c"}, + {file = "pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585"}, ] [package.dependencies] @@ -3047,29 +3047,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.9.6" +version = "0.9.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.9.6-py3-none-linux_armv6l.whl", hash = "sha256:2f218f356dd2d995839f1941322ff021c72a492c470f0b26a34f844c29cdf5ba"}, - {file = "ruff-0.9.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b908ff4df65dad7b251c9968a2e4560836d8f5487c2f0cc238321ed951ea0504"}, - {file = "ruff-0.9.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b109c0ad2ececf42e75fa99dc4043ff72a357436bb171900714a9ea581ddef83"}, - {file = "ruff-0.9.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de4367cca3dac99bcbd15c161404e849bb0bfd543664db39232648dc00112dc"}, - {file = "ruff-0.9.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3ee4d7c2c92ddfdaedf0bf31b2b176fa7aa8950efc454628d477394d35638b"}, - {file = "ruff-0.9.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dc1edd1775270e6aa2386119aea692039781429f0be1e0949ea5884e011aa8e"}, - {file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4a091729086dffa4bd070aa5dab7e39cc6b9d62eb2bef8f3d91172d30d599666"}, - {file = "ruff-0.9.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1bbc6808bf7b15796cef0815e1dfb796fbd383e7dbd4334709642649625e7c5"}, - {file = "ruff-0.9.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:589d1d9f25b5754ff230dce914a174a7c951a85a4e9270613a2b74231fdac2f5"}, - {file = "ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc61dd5131742e21103fbbdcad683a8813be0e3c204472d520d9a5021ca8b217"}, - {file = "ruff-0.9.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5e2d9126161d0357e5c8f30b0bd6168d2c3872372f14481136d13de9937f79b6"}, - {file = "ruff-0.9.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:68660eab1a8e65babb5229a1f97b46e3120923757a68b5413d8561f8a85d4897"}, - {file = "ruff-0.9.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c4cae6c4cc7b9b4017c71114115db0445b00a16de3bcde0946273e8392856f08"}, - {file = "ruff-0.9.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19f505b643228b417c1111a2a536424ddde0db4ef9023b9e04a46ed8a1cb4656"}, - {file = "ruff-0.9.6-py3-none-win32.whl", hash = "sha256:194d8402bceef1b31164909540a597e0d913c0e4952015a5b40e28c146121b5d"}, - {file = "ruff-0.9.6-py3-none-win_amd64.whl", hash = "sha256:03482d5c09d90d4ee3f40d97578423698ad895c87314c4de39ed2af945633caa"}, - {file = "ruff-0.9.6-py3-none-win_arm64.whl", hash = "sha256:0e2bb706a2be7ddfea4a4af918562fdc1bcb16df255e5fa595bbd800ce322a5a"}, - {file = "ruff-0.9.6.tar.gz", hash = "sha256:81761592f72b620ec8fa1068a6fd00e98a5ebee342a3642efd84454f3031dca9"}, + {file = "ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367"}, + {file = "ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7"}, + {file = "ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb"}, + {file = "ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0"}, + {file = "ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17"}, + {file = "ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1"}, + {file = "ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57"}, + {file = "ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e"}, + {file = "ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1"}, + {file = "ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1"}, + {file = "ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf"}, + {file = "ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933"}, ] [[package]] diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 7446435388..fdb06718e9 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.126.1" +version = "1.129.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile-v2/.gitignore b/mobile-v2/.gitignore index 617250fec0..017b8f34ec 100644 --- a/mobile-v2/.gitignore +++ b/mobile-v2/.gitignore @@ -50,3 +50,4 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/android/app/.cxx diff --git a/mobile-v2/lib/domain/services/login.service.dart b/mobile-v2/lib/domain/services/login.service.dart index 72f0413df4..f3d4d9e4d2 100644 --- a/mobile-v2/lib/domain/services/login.service.dart +++ b/mobile-v2/lib/domain/services/login.service.dart @@ -175,5 +175,6 @@ class LoginService with LogMixin { await di().deleteAll(); await di().deleteAll(); await di().deleteAll(); + await di().delete(StoreKey.accessToken); } } diff --git a/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart b/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart index bd258891ea..14ae096ae1 100644 --- a/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart +++ b/mobile-v2/lib/presentation/router/pages/splash_screen.page.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/services/login.service.dart'; import 'package:immich_mobile/presentation/components/image/immich_logo.widget.dart'; import 'package:immich_mobile/presentation/modules/login/states/login_page.state.dart'; import 'package:immich_mobile/presentation/router/router.dart'; +import 'package:immich_mobile/presentation/states/gallery_permission.state.dart'; import 'package:immich_mobile/service_locator.dart'; import 'package:immich_mobile/utils/mixins/log.mixin.dart'; @@ -39,6 +40,7 @@ class _SplashScreenState extends State duration: const Duration(seconds: 30), vsync: this, )..repeat(); + unawaited(di().requestPermission()); } @override diff --git a/mobile-v2/lib/utils/log_manager.dart b/mobile-v2/lib/utils/log_manager.dart index 683df045f0..911fe264c3 100644 --- a/mobile-v2/lib/utils/log_manager.dart +++ b/mobile-v2/lib/utils/log_manager.dart @@ -85,6 +85,7 @@ class LogManager { void dispose() { unawaited(_subscription.cancel()); + _timer?.cancel(); } Future clearLogs() async { diff --git a/mobile-v2/pubspec.lock b/mobile-v2/pubspec.lock index 30af1573dc..a34c637e77 100644 --- a/mobile-v2/pubspec.lock +++ b/mobile-v2/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" analyzer_plugin: dependency: transitive description: @@ -42,10 +42,10 @@ packages: dependency: "direct main" description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" auto_route: dependency: "direct main" description: @@ -82,10 +82,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: @@ -178,10 +178,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" charcode: dependency: transitive description: @@ -218,10 +218,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -234,10 +234,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" color: dependency: transitive description: @@ -370,10 +370,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" faker: dependency: "direct dev" description: @@ -661,18 +661,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -701,18 +701,18 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -733,10 +733,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mime: dependency: transitive description: @@ -796,10 +796,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_parsing: dependency: transitive description: @@ -1028,7 +1028,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" slang: dependency: "direct main" description: @@ -1065,10 +1065,10 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -1121,18 +1121,18 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -1145,10 +1145,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" synchronized: dependency: transitive description: @@ -1161,18 +1161,18 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" time: dependency: transitive description: @@ -1297,10 +1297,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.1" watcher: dependency: transitive description: @@ -1390,5 +1390,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.24.0" diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 6794f39b81..a44e508088 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -67,26 +67,27 @@ custom_lint: - lib/entities/*.entity.dart - lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart - lib/infrastructure/entities/*.entity.dart - - lib/infrastructure/repositories/{store,db}.repository.dart + - lib/infrastructure/repositories/{store,db,log}.repository.dart - lib/providers/infrastructure/db.provider.dart # acceptable exceptions for the time being (until Isar is fully replaced) + - lib/providers/app_life_cycle.provider.dart - integration_test/test_utils/general_helper.dart - lib/main.dart - lib/pages/album/album_asset_selection.page.dart - lib/routing/router.dart - lib/services/immich_logger.service.dart # not really a service... more a util - lib/utils/{db,migration}.dart + - lib/utils/bootstrap.dart - lib/widgets/asset_grid/asset_grid_data_structure.dart - test/**.dart # refactor the remaining providers - - lib/providers/{db,user}.provider.dart - - lib/providers/backup/backup.provider.dart + - lib/providers/db.provider.dart - import_rule_openapi: message: openapi must only be used through ApiRepositories restrict: package:openapi allowed: - # requried / wanted + # required / wanted - lib/repositories/*_api.repository.dart # acceptable exceptions for the time being - lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index f46a0dd4ec..1356c468ab 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 184, - "android.injected.version.name" => "1.126.1", + "android.injected.version.code" => 187, + "android.injected.version.name" => "1.129.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/assets/i18n/ca-CA.json b/mobile/assets/i18n/ca.json similarity index 63% rename from mobile/assets/i18n/ca-CA.json rename to mobile/assets/i18n/ca.json index 8366e01f93..e41ab4b183 100644 --- a/mobile/assets/i18n/ca-CA.json +++ b/mobile/assets/i18n/ca.json @@ -1,24 +1,35 @@ { - "action_common_cancel": "Cancel·la", - "action_common_update": "Actualitza", - "add_to_album_bottom_sheet_added": "S'ha afegit a {album}", - "add_to_album_bottom_sheet_already_exists": "Ja es troba en {album}", + "action_common_back": "Enrere", + "action_common_cancel": "Cancel·lar", + "action_common_clear": "Buida", + "action_common_confirm": "Confirmar", + "action_common_save": "Desa", + "action_common_select": "Selecciona", + "action_common_update": "Actualitzar", + "add_a_name": "Afegeix un nom", + "add_endpoint": "afegir endpoint", + "add_to_album_bottom_sheet_added": "Added to {album}", + "add_to_album_bottom_sheet_already_exists": "Already in {album}", "advanced_settings_log_level_title": "Log level: {}", "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", "advanced_settings_prefer_remote_title": "Prefereix imatges remotes", + "advanced_settings_proxy_headers_subtitle": "Definiu les capçaleres de proxy que Immich per enviar amb cada sol·licitud de xarxa", + "advanced_settings_proxy_headers_title": "Capçaleres de proxy", "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", - "advanced_settings_tile_subtitle": "Configuració avançada", + "advanced_settings_tile_subtitle": "Advanced user's settings", "advanced_settings_tile_title": "Avançat", "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", "advanced_settings_troubleshooting_title": "Resolució de problemes", "album_info_card_backup_album_excluded": "Exclosos", "album_info_card_backup_album_included": "Inclosos", - "album_thumbnail_card_item": "1 element", - "album_thumbnail_card_items": "{} elements", - "album_thumbnail_card_shared": " · Compartit", + "albums": "Àlbums", + "album_thumbnail_card_item": "1 item", + "album_thumbnail_card_items": "{} items", + "album_thumbnail_card_shared": " · Shared", "album_thumbnail_owned": "Owned", "album_thumbnail_shared_by": "Compartit per {}", + "album_viewer_appbar_delete_confirm": "Confirmes que vols suprimir aquest àlbum del teu compte?", "album_viewer_appbar_share_delete": "Esborra l'àlbum", "album_viewer_appbar_share_err_delete": "Error al esborrar l'àlbum", "album_viewer_appbar_share_err_leave": "Error al sortir de l'àlbum", @@ -28,25 +39,39 @@ "album_viewer_appbar_share_remove": "Treu de l'àlbum", "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Afegeix usuaris", + "all": "Tot", "all_people_page_title": "Persones", "all_videos_page_title": "Vídeos", "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", + "archived": "Arxivat", "archive_page_no_archived_assets": "No s'ha trobat res arxivat", "archive_page_title": "Arxiu({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", - "asset_list_group_by_sub_title": "Group by", + "asset_action_delete_err_read_only": "No es poden esborrar el fitxer(s) de només lectura, ometent", + "asset_action_share_err_offline": "No s'ha pogut obtenir el fitxer(s) sense connexió, ometent", + "asset_list_group_by_sub_title": "Agrupar per", "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", "asset_list_layout_settings_group_automatically": "Automàtic", "asset_list_layout_settings_group_by": "Group assets by", "asset_list_layout_settings_group_by_month": "Month", "asset_list_layout_settings_group_by_month_day": "Month + day", - "asset_list_layout_sub_title": "Layout", + "asset_list_layout_sub_title": "Disseny", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", - "asset_viewer_settings_title": "Asset Viewer", + "asset_restored_successfully": "Element recuperat correctament", + "assets_deleted_permanently": "{} element(s) esborrats permanentment", + "assets_deleted_permanently_from_server": "{} element(s) esborrats permanentment del servidor d'Immich", + "assets_removed_permanently_from_device": "{} element(s) esborrat permanentment del dispositiu", + "assets_restored_successfully": "{} element(s) recuperats correctament", + "assets_trashed": "{} element(s) enviat a la paperera", + "assets_trashed_from_server": "{} element(s) enviat a la paperera del servidor d'Immich", + "asset_viewer_settings_subtitle": "Gestiona la configuració del visualitzador de la galeria", + "asset_viewer_settings_title": "Visor d'arxius", + "automatic_endpoint_switching_subtitle": "Connecteu-vos localment a través de la Wi-Fi designada quan estigui disponible i utilitzeu connexions alternatives en altres llocs", + "automatic_endpoint_switching_title": "Canvi automàtic d'URL", + "background_location_permission": "Permís d'ubicació en segon pla", + "background_location_permission_content": "Per canviar de xarxa quan s'executa en segon pla, Immich ha de *sempre* tenir accés a la ubicació precisa perquè l'aplicació pugui llegir el nom de la xarxa Wi-Fi", "backup_album_selection_page_albums_device": "Àlbums al dispositiu ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -111,6 +136,8 @@ "backup_manual_in_progress": "Upload already in progress. Try after sometime", "backup_manual_success": "Success", "backup_manual_title": "Upload status", + "backup_options_page_title": "Opcions de còpia de seguretat", + "backup_setting_subtitle": "Gestiona la configuració de càrrega en segon pla i en primer pla", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", "cache_settings_clear_cache_button": "Clear cache", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", @@ -129,73 +156,136 @@ "cache_settings_tile_subtitle": "Control the local storage behaviour", "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Configuració de la memòria cau", + "cancel": "Cancel·la", + "canceled": "Cancel·lat", + "change_display_order": "Canvia l'ordre de visualització", "change_password_form_confirm_password": "Confirma la contrasenya", "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", + "check_corrupt_asset_backup": "Comprovar les còpies de seguretat corruptes", + "check_corrupt_asset_backup_button": "Realitzar comprovació", + "check_corrupt_asset_backup_description": "Executeu aquesta comprovació només mitjançant Wi-Fi i un cop s'hagi fet una còpia de seguretat de tots els actius. El procediment pot trigar uns minuts.", + "client_cert_dialog_msg_confirm": "OK", + "client_cert_enter_password": "Introdueix la contrasenya", + "client_cert_import": "Importar", + "client_cert_import_success_msg": "S'ha importat el certificat del client", + "client_cert_invalid_msg": "Fitxer de certificat no vàlid o contrasenya incorrecta", + "client_cert_remove": "Eliminar", + "client_cert_remove_msg": "S'ha eliminat el certificat del client", + "client_cert_subtitle": "Només admet el format PKCS12 (.p12, .pfx). La importació/eliminació de certificats només està disponible abans d'iniciar sessió", + "client_cert_title": "Certificat de client SSL", "common_add_to_album": "Add to album", "common_change_password": "Change Password", "common_create_new_album": "Crea un àlbum nou", "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", "common_shared": "Compartit", + "completed": "Completat", + "contextual_search": "Sortida del sol a la platja", "control_bottom_app_bar_add_to_album": "Add to album", "control_bottom_app_bar_album_info": "{} elements", "control_bottom_app_bar_album_info_shared": "{} elements - Compartits", "control_bottom_app_bar_archive": "Arxiu", "control_bottom_app_bar_create_new_album": "Crea un àlbum nou", "control_bottom_app_bar_delete": "Esborra", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", + "control_bottom_app_bar_delete_from_immich": "Suprimeix del Immich", + "control_bottom_app_bar_delete_from_local": "Suprimeix del dispositiu", + "control_bottom_app_bar_download": "Descarrega", + "control_bottom_app_bar_edit": "Edita", "control_bottom_app_bar_edit_location": "Edit Location", "control_bottom_app_bar_edit_time": "Edit Date & Time", "control_bottom_app_bar_favorite": "Preferit", "control_bottom_app_bar_share": "Share", "control_bottom_app_bar_share_to": "Share To", "control_bottom_app_bar_stack": "Stack", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_trash_from_immich": "Mou a paperera", "control_bottom_app_bar_unarchive": "Desarxiva", "control_bottom_app_bar_unfavorite": "Unfavorite", "control_bottom_app_bar_upload": "Upload", + "create_album": "Crear àlbum", "create_album_page_untitled": "Untitled", + "create_new": "CREAR NOU", "create_shared_album_page_create": "Create", "create_shared_album_page_share": "Comparteix", "create_shared_album_page_share_add_assets": "AFEGEIX ELEMENTS", "create_shared_album_page_share_select_photos": "Escull fotografies", + "crop": "Retalla", "curated_location_page_title": "Localitzacions", "curated_object_page_title": "Coses", + "current_server_address": "Current server address", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", "delete_dialog_alert": "These items will be permanently deleted from Immich and from your device", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_alert_local": "Aquests elements s'eliminaran permanentment del vostre dispositiu, però encara estaran disponibles al servidor Immich", + "delete_dialog_alert_local_non_backed_up": "Alguns dels elements no tenen còpia de seguretat a Immich i s'eliminaran permanentment del dispositiu", + "delete_dialog_alert_remote": "Aquests elements s'eliminaran permanentment del servidor Immich", "delete_dialog_cancel": "Cancel·la", "delete_dialog_ok": "Esborra", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_ok_force": "Suprimeix de totes maneres", "delete_dialog_title": "Esborra permanentment", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "Esborrar només les que tinguin còpia de seguretat", + "delete_local_dialog_ok_force": "Suprimeix de totes maneres", "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Afegeix descripció...", "description_input_submit_error": "Error updating description, check the log for more details", - "edit_date_time_dialog_date_time": "Date and Time", - "edit_date_time_dialog_timezone": "Timezone", - "edit_location_dialog_title": "Location", + "description_search": "Jornada de senderisme a Sapa", + "download_canceled": "Descàrrega cancel·lada", + "download_complete": "Descàrrega completada", + "download_enqueue": "Descàrrega en cua", + "download_error": "Error de descàrrega", + "download_failed": "Descàrrega ha fallat", + "download_filename": "arxiu: {}", + "download_finished": "Descàrrega acabada", + "downloading": "Descarregant...", + "downloading_media": "Descàrrega multimèdia", + "download_notfound": "No s'ha trobat la descàrrega", + "download_paused": "Descàrrega pausada", + "download_started": "Descàrrega ha començat", + "download_sucess": "Descarregat amb èxit", + "download_sucess_android": "El multimedia s'ha descarregat a DCIM/Immich", + "download_waiting_to_retry": "Esperant per tornar-ho a intentar", + "edit_date_time_dialog_date_time": "Data i Hora", + "edit_date_time_dialog_search_timezone": "Cerca zona horària...", + "edit_date_time_dialog_timezone": "Zona horària", + "edit_image_title": "Editar", + "edit_location_dialog_title": "Ubicació", + "end_date": "Data final", + "enqueued": "En cua", + "enter_wifi_name": "Introdueix el nom de WiFi", + "error_change_sort_album": "No s'ha pogut canviar l'ordre d'ordenació dels àlbums", + "error_saving_image": "Error: {}", "exif_bottom_sheet_description": "Afegeix descripció", "exif_bottom_sheet_details": "DETALLS", "exif_bottom_sheet_location": "UBICACIÓ", "exif_bottom_sheet_location_add": "Add a location", - "exif_bottom_sheet_people": "PEOPLE", - "exif_bottom_sheet_person_add_person": "Add name", + "exif_bottom_sheet_people": "PERSONES", + "exif_bottom_sheet_person_add_person": "Afegir nom", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", "experimental_settings_title": "Experimental", + "external_network": "Xarxa externa", + "external_network_sheet_info": "Quan no estigui a la xarxa WiFi preferida, l'aplicació es connectarà al servidor mitjançant el primer dels URL següents a què pot arribar, començant de dalt a baix.", + "failed": "Fallat", + "favorites": "Favorits", "favorites_page_no_favorites": "No s'han trobat preferits", "favorites_page_title": "Favorites", + "filename_search": "Nom o extensió del fitxer", + "filter": "Filtrar", + "get_wifiname_error": "No s'ha pogut obtenir el nom de la Wi-Fi. Assegureu-vos que heu concedit els permisos necessaris i que esteu connectat a una xarxa Wi-Fi", + "grant_permission": "Grant permission", + "haptic_feedback_switch": "Activa la resposta hàptica", + "haptic_feedback_title": "Resposta Hàptica", + "header_settings_add_header_tip": "Afegeix Capçalera", + "header_settings_field_validator_msg": "El valor no pot estar buit", + "header_settings_header_name_input": "Nom de la capçalera", + "header_settings_header_value_input": "Valor de la capçalera", + "header_settings_page_title": "Capçaleres de proxy", + "headers_settings_tile_subtitle": "Definiu les capçaleres de proxy que l'aplicació hauria d'enviar amb cada sol·licitud de xarxa", + "headers_settings_tile_title": "Custom proxy headers", "home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.", "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", "home_page_add_to_album_success": "Added {added} assets to album {album}.", @@ -204,16 +294,22 @@ "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Building the timeline", "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", + "home_page_delete_remote_err_local": "Elements locals a la selecció d'eliminació remota, ometent", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "ignore_icloud_photos": "Ignora fotos d'iCloud", + "ignore_icloud_photos_description": "Les fotos emmagatzemades a iCloud no es penjaran al servidor Immich", + "image_saved_successfully": "Imatge desada", "image_viewer_page_state_provider_download_error": "Download Error", - "image_viewer_page_state_provider_download_started": "Download Started", + "image_viewer_page_state_provider_download_started": "Descàrrega començada", "image_viewer_page_state_provider_download_success": "Download Success", "image_viewer_page_state_provider_share_error": "Share Error", + "invalid_date": "Data invàlida", + "invalid_date_format": "Format de data invàlid", + "library": "Llibreria", "library_page_albums": "Àlbums", "library_page_archive": "Arxiu", "library_page_device_albums": "Àlbums al Dispositiu", @@ -226,13 +322,17 @@ "library_page_sort_most_oldest_photo": "Oldest photo", "library_page_sort_most_recent_photo": "Most recent photo", "library_page_sort_title": "Album title", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude": "Latitude", - "location_picker_latitude_error": "Enter a valid latitude", + "local_network": "Xarxa local", + "local_network_sheet_info": "L'aplicació es connectarà al servidor mitjançant aquest URL quan utilitzeu la xarxa Wi-Fi especificada", + "location_permission": "Permís d'ubicació", + "location_permission_content": "Per utilitzar la funció de canvi automàtic, Immich necessita un permís de ubicació precisa perquè pugui llegir el nom de la xarxa WiFi actual", + "location_picker_choose_on_map": "Escollir en el mapa", + "location_picker_latitude": "Latitud", + "location_picker_latitude_error": "Introdueix una latitud vàlida", "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude": "Longitude", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", + "location_picker_longitude": "Longitud", + "location_picker_longitude_error": "Introdueix una longitud vàlida", + "location_picker_longitude_hint": "Introdueix aquí la longitud", "login_disabled": "Login has been disabled", "login_form_api_exception": "API exception. Please check the server URL and try again.", "login_form_back_button_text": "Back", @@ -258,12 +358,12 @@ "login_form_server_error": "Could not connect to server.", "login_password_changed_error": "There was an error updating your password", "login_password_changed_success": "Password updated successfully", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", + "map_assets_in_bound": "{} foto", + "map_assets_in_bounds": "{} fotos", "map_cannot_get_user_location": "Cannot get user's location", "map_location_dialog_cancel": "Cancel", "map_location_dialog_yes": "Yes", - "map_location_picker_page_use_location": "Use this location", + "map_location_picker_page_use_location": "Utilitzar aquesta ubicació", "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", "map_location_service_disabled_title": "Location Service disabled", "map_no_assets_in_bounds": "No photos in this area", @@ -271,32 +371,44 @@ "map_no_location_permission_title": "Location Permission denied", "map_settings_dark_mode": "Dark mode", "map_settings_date_range_option_all": "All", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", + "map_settings_date_range_option_day": "Últimes 24 hores", + "map_settings_date_range_option_days": "Darrers {} dies", + "map_settings_date_range_option_year": "Any passat", + "map_settings_date_range_option_years": "Darrers {} anys", "map_settings_dialog_cancel": "Cancel", "map_settings_dialog_save": "Save", "map_settings_dialog_title": "Map Settings", "map_settings_include_show_archived": "Include Archived", + "map_settings_include_show_partners": "Incloure companys", "map_settings_only_relative_range": "Date range", "map_settings_only_show_favorites": "Show Favorite Only", - "map_settings_theme_settings": "Map Theme", + "map_settings_theme_settings": "Tema del Mapa", "map_zoom_to_see_photos": "Zoom out to see photos", - "memories_all_caught_up": "All caught up", - "memories_check_back_tomorrow": "Check back tomorrow for more memories", - "memories_start_over": "Start Over", - "memories_swipe_to_close": "Swipe up to close", + "memories_all_caught_up": "Posat al dia", + "memories_check_back_tomorrow": "Torna demà per veure més records", + "memories_start_over": "Torna a començar", + "memories_swipe_to_close": "Llisca per tancar", + "memories_year_ago": "Fa un any", + "memories_years_ago": "Fa {} anys", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Motion Photos", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "No es pot canviar la data del fitxer(s) de només lectura, ometent", + "multiselect_grid_edit_gps_err_read_only": "No es pot canviar la localització de fitxers de només lectura. Saltant.", + "my_albums": "Els meus àlbums", + "networking_settings": "Xarxes", + "networking_subtitle": "Gestiona la configuració del endpoint del servidor", + "no_assets_to_show": "No hi ha elements per mostrar", + "no_name": "Sense nom", "notification_permission_dialog_cancel": "Cancel·la", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_dialog_settings": "Configuració", "notification_permission_list_tile_content": "Grant permission to enable notifications.", "notification_permission_list_tile_enable_button": "Activa les notificacions", "notification_permission_list_tile_title": "Notification Permission", + "not_selected": "No seleccionat", + "on_this_device": "En aquest dispositiu", + "partner_list_user_photos": "fotos de {user}", + "partner_list_view_all": "Veure tot", "partner_page_add_partner": "Afegeix company", "partner_page_empty_message": "Your photos are not yet shared with any partner.", "partner_page_no_more_users": "No more users to add", @@ -306,6 +418,9 @@ "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Company", + "partners": "Companys", + "paused": "Pausat", + "people": "Persones", "permission_onboarding_back": "Back", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_get_started": "Get started", @@ -316,7 +431,9 @@ "permission_onboarding_permission_granted": "Permission granted! You are all set.", "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", "permission_onboarding_request": "Immich requires permission to view your photos and videos.", - "preferences_settings_title": "Preferences", + "places": "Llocs", + "preferences_settings_subtitle": "Gestiona les preferències de l'aplicació", + "preferences_settings_title": "Preferències", "profile_drawer_app_logs": "Logs", "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", @@ -328,9 +445,44 @@ "profile_drawer_settings": "Settings", "profile_drawer_sign_out": "Tanca la sessió", "profile_drawer_trash": "Trash", + "recently_added": "Afegit recentment", "recently_added_page_title": "Recently Added", - "scaffold_body_error_occurred": "Error occurred", + "save": "Desa", + "save_to_gallery": "Desa a galeria", + "scaffold_body_error_occurred": "S'ha produït un error", + "search_albums": "Cerca àlbums", "search_bar_hint": "Search your photos", + "search_filter_apply": "Aplicar filtre", + "search_filter_camera": "Càmera", + "search_filter_camera_make": "Marca", + "search_filter_camera_model": "Model", + "search_filter_camera_title": "Selecciona el tipus de càmera", + "search_filter_contextual": "Cerca per contexte", + "search_filter_date": "Data", + "search_filter_date_interval": "{start} a {end}", + "search_filter_date_title": "Selecciona un rang de dates", + "search_filter_description": "Cerca per descripció", + "search_filter_display_option_archive": "Arxivat", + "search_filter_display_option_favorite": "Favorit", + "search_filter_display_option_not_in_album": "No en àlbum", + "search_filter_display_options": "Opcions de Visualització", + "search_filter_display_options_title": "Opcions de visualització", + "search_filter_filename": "Cerca pel nom del fitxer", + "search_filter_location": "Ubicació", + "search_filter_location_city": "Ciutat", + "search_filter_location_country": "País", + "search_filter_location_state": "Estat", + "search_filter_location_title": "Selecciona l'ubicació", + "search_filter_media_type": "Tipus de multimèdia", + "search_filter_media_type_all": "Tot", + "search_filter_media_type_image": "Imatge", + "search_filter_media_type_title": "Selecciona tipus de multimèdia", + "search_filter_media_type_video": "Vídeo", + "search_filter_people": "Persones", + "search_filter_people_hint": "Filtra persones", + "search_filter_people_title": "Selecciona persones", + "search_no_more_result": "No més resultats", + "search_no_result": "No s'han trobat resultats, proveu un terme de cerca o una combinació diferents", "search_page_categories": "Categories", "search_page_favorites": "Preferides", "search_page_motion_photos": "Fotografies animades", @@ -347,6 +499,7 @@ "search_page_places": "Llocs", "search_page_recently_added": "Afegit recentment", "search_page_screenshots": "Captures de pantalla", + "search_page_search_photos_videos": "Cerca les teves fotos i vídeos", "search_page_selfies": "Autofotos", "search_page_things": "Coses", "search_page_videos": "Videos", @@ -359,6 +512,7 @@ "select_additional_user_for_sharing_page_suggestions": "Suggeriments", "select_user_for_sharing_page_err_album": "Error al crear l'àlbum", "select_user_for_sharing_page_share_suggestions": "Suggestions", + "server_endpoint": "Endpoint de Servidor", "server_info_box_app_version": "Versió de l'aplicació", "server_info_box_latest_release": "Latest Version", "server_info_box_server_url": "Server URL", @@ -368,6 +522,10 @@ "setting_image_viewer_original_title": "Load original image", "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", "setting_image_viewer_preview_title": "Load preview image", + "setting_image_viewer_title": "Imatges", + "setting_languages_apply": "Aplicar", + "setting_languages_subtitle": "Canvia el llenguatge de l'aplicació", + "setting_languages_title": "Idiomes", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_hours": "{} hours", "setting_notifications_notify_immediately": "immediately", @@ -382,9 +540,15 @@ "setting_notifications_total_progress_title": "Show background backup total progress", "setting_pages_app_bar_settings": "Settings", "settings_require_restart": "Please restart Immich to apply this setting", + "setting_video_viewer_looping_subtitle": "Habilita per reproduir automàticament un vídeo al visualitzador de detalls.", + "setting_video_viewer_looping_title": "Bucle", + "setting_video_viewer_original_video_subtitle": "Quan reproduïu un vídeo des del servidor, reproduïu l'original encara que hi hagi una transcodificació disponible. Pot conduir a l'amortització. Els vídeos disponibles localment es reprodueixen en qualitat original independentment d'aquesta configuració.", + "setting_video_viewer_original_video_title": "Força el vídeo original", + "setting_video_viewer_title": "Vídeos", "share_add": "Afegeix", "share_add_photos": "Afegeix fotografies", "share_add_title": "Afegeix un títol", + "share_assets_selected": "{} seleccionats", "share_create_album": "Crea un àlbum", "shared_album_activities_input_disable": "Comment is disabled", "shared_album_activities_input_hint": "Say something", @@ -398,6 +562,7 @@ "shared_album_section_people_owner_label": "Owner", "shared_album_section_people_title": "PEOPLE", "share_dialog_preparing": "Preparing...", + "shared_intent_upload_button_progress_text": "{} / {} Pujat", "shared_link_app_bar_title": "Shared Links", "shared_link_clipboard_copied_massage": "Copied to clipboard", "shared_link_clipboard_text": "Link: {}\nPassword: {}", @@ -412,13 +577,15 @@ "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", "shared_link_edit_expire_after": "Expire after", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", + "shared_link_edit_expire_after_option_day": "1 dia", + "shared_link_edit_expire_after_option_days": "{} dies", + "shared_link_edit_expire_after_option_hour": "1 hora", + "shared_link_edit_expire_after_option_hours": "{} hores", + "shared_link_edit_expire_after_option_minute": "1 minut", + "shared_link_edit_expire_after_option_minutes": "{} minuts", + "shared_link_edit_expire_after_option_months": "{} mesos", "shared_link_edit_expire_after_option_never": "Never", + "shared_link_edit_expire_after_option_year": "any {}", "shared_link_edit_password": "Password", "shared_link_edit_password_hint": "Enter the share password", "shared_link_edit_show_meta": "Show metadata", @@ -426,65 +593,89 @@ "shared_link_empty": "You don't have any shared links", "shared_link_error_server_url_fetch": "Cannot fetch the server url", "shared_link_expired": "Expired", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", + "shared_link_expires_day": "Caduca d'aquí a {} dia", + "shared_link_expires_days": "Caduca d'aquí a {} dies", + "shared_link_expires_hour": "Caduca d'aquí a {} hora", + "shared_link_expires_hours": "Caduca d'aquí a {} hores", + "shared_link_expires_minute": "Caduca d'aquí a {} minut", "shared_link_expires_minutes": "Expires in {} minutes", "shared_link_expires_never": "Expires ∞", - "shared_link_expires_second": "Expires in {} second", + "shared_link_expires_second": "Caduca d'aquí a {} segon", "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_info_chip_download": "Baixa", + "shared_link_individual_shared": "Individual compartit", + "shared_link_info_chip_download": "Download", "shared_link_info_chip_metadata": "EXIF", "shared_link_info_chip_upload": "Puja", "shared_link_manage_links": "Manage Shared links", - "share_done": "Fet", + "shared_link_public_album": "Àlbum públic", + "shared_links": "Enllaços compartits", + "share_done": "Done", + "shared_with_me": "Compartit amb mi", "share_invite": "Convida a l'àlbum", - "sharing_page_album": "Àlbums compartits", + "sharing_page_album": "Shared albums", "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", "sharing_page_empty_list": "EMPTY LIST", "sharing_silver_appbar_create_shared_album": "Crea àlbum compartit", "sharing_silver_appbar_shared_links": "Shared links", "sharing_silver_appbar_share_partner": "Comparteix amb un company", - "tab_controller_nav_library": "Bibilioteca", - "tab_controller_nav_photos": "Fotos", + "start_date": "Data inicial", + "sync": "Sincronitzar", + "sync_albums": "Sincronitzar àlbums", + "sync_albums_manual_subtitle": "Sincronitza tots els vídeos i fotos penjats amb els àlbums de còpia de seguretat seleccionats", + "sync_upload_album_setting_subtitle": "Creeu i pugeu les seves fotos i vídeos als àlbums seleccionats a Immich", + "tab_controller_nav_library": "Library", + "tab_controller_nav_photos": "Fotografies", "tab_controller_nav_search": "Cerca", "tab_controller_nav_sharing": "Compartint", "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", - "theme_setting_dark_mode_switch": "Modes fosc", + "theme_setting_colorful_interface_subtitle": "Apliqueu color primari a les superfícies de fons.", + "theme_setting_colorful_interface_title": "Interfície colorida", + "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_subtitle": "Trieu un color per a les accions i els accents principals.", + "theme_setting_primary_color_title": "Color primari", + "theme_setting_system_primary_color_title": "Utilitza color de sistema", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", - "theme_setting_theme_title": "Tema", + "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", "translated_text_options": "Options", - "trash_page_delete": "Elimina", - "trash_page_delete_all": "Elimina-ho tot", - "trash_page_empty_trash_btn": "Buida la paperera", + "trash": "Paperera", + "trash_emptied": "Paperera buidada", + "trash_page_delete": "Delete", + "trash_page_delete_all": "Delete All", + "trash_page_empty_trash_btn": "Empty trash", "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", "trash_page_empty_trash_dialog_ok": "Ok", "trash_page_info": "Trashed items will be permanently deleted after {} days", "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Recupera", - "trash_page_restore_all": "Recupera-ho tot", + "trash_page_restore": "Restore", + "trash_page_restore_all": "Restore All", "trash_page_select_assets_btn": "Select assets", "trash_page_select_btn": "Select", "trash_page_title": "Trash ({})", - "upload_dialog_cancel": "Cancel·la", + "upload": "Puja", + "upload_dialog_cancel": "Cancel", "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_ok": "Upload", "upload_dialog_title": "Upload Asset", + "uploading": "Pujant", + "upload_to_immich": "Puja a Immich ({})", + "use_current_connection": "utilitzar la connexió actual", + "validate_endpoint_error": "Per favor introdueix un URL vàlid", "version_announcement_overlay_ack": "Acknowledge", "version_announcement_overlay_release_notes": "release notes", "version_announcement_overlay_text_1": "Hi friend, there is a new release of", "version_announcement_overlay_text_2": "please take your time to visit the ", "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", + "videos": "Vídeos", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" -} + "viewer_unstack": "Un-Stack", + "wifi_name": "Nom WiFi", + "your_wifi_name": "El teu nom WiFi" +} \ No newline at end of file diff --git a/mobile/assets/i18n/gl-ES.json b/mobile/assets/i18n/gl-ES.json index 9450b4b44f..a5b43d7447 100644 --- a/mobile/assets/i18n/gl-ES.json +++ b/mobile/assets/i18n/gl-ES.json @@ -133,7 +133,7 @@ "backup_info_card_assets": "assets", "backup_manual_cancelled": "Cancelled", "backup_manual_failed": "Failed", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", + "backup_manual_in_progress": "Upload already in progress. Try after some time", "backup_manual_success": "Success", "backup_manual_title": "Upload status", "backup_options_page_title": "Backup options", diff --git a/mobile/assets/i18n/uk-UA.json b/mobile/assets/i18n/uk-UA.json index d4432b88c9..2b619099da 100644 --- a/mobile/assets/i18n/uk-UA.json +++ b/mobile/assets/i18n/uk-UA.json @@ -7,10 +7,10 @@ "action_common_select": "Вибрати", "action_common_update": "Оновити", "add_a_name": "Додати ім'я", - "add_endpoint": "Add endpoint", + "add_endpoint": "Додати кінцеву точку", "add_to_album_bottom_sheet_added": "Додано до {album}", "add_to_album_bottom_sheet_already_exists": "Вже є в {album}", - "advanced_settings_log_level_title": "Log level: {}", + "advanced_settings_log_level_title": "Рівень логування: {}", "advanced_settings_prefer_remote_subtitle": "Деякі пристрої вельми повільно завантажують мініатюри із елементів на пристрої. Активуйте для завантаження віддалених мініатюр натомість.", "advanced_settings_prefer_remote_title": "Перевага віддаленим зображенням", "advanced_settings_proxy_headers_subtitle": "Визначте заголовки проксі-сервера, які Immich має надсилати з кожним мережевим запитом.", @@ -66,12 +66,12 @@ "assets_restored_successfully": "{} елемент(и) успішно відновлено", "assets_trashed": "{} елемент(и) поміщено до кошика", "assets_trashed_from_server": "{} елемент(и) поміщено до кошика на сервері Immich", - "asset_viewer_settings_subtitle": "Manage your gallery viewer settings", + "asset_viewer_settings_subtitle": "Керуйте налаштуваннями переглядача галереї", "asset_viewer_settings_title": "Переглядач зображень", - "automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere", - "automatic_endpoint_switching_title": "Automatic URL switching", - "background_location_permission": "Background location permission", - "background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name", + "automatic_endpoint_switching_subtitle": "Підключатися локально через зазначену Wi-Fi мережу, коли це можливо, і використовувати альтернативні з'єднання в інших випадках", + "automatic_endpoint_switching_title": "Автоматичне перемикання URL", + "background_location_permission": "Дозвіл до місцезнаходження у фоні", + "background_location_permission_content": "Щоб перемикати мережі у фоновому режимі, Immich має *завжди* мати доступ до точної геолокації, щоб зчитувати назву Wi-Fi мережі", "backup_album_selection_page_albums_device": "Альбоми на пристрої ({})", "backup_album_selection_page_albums_tap": "Торкніться, щоб включити,\nторкніться двічі, щоб виключити", "backup_album_selection_page_assets_scatter": "Елементи можуть належати до кількох альбомів водночас. Таким чином, альбоми можуть бути включені або вилучені під час резервного копіювання.", @@ -119,7 +119,7 @@ "backup_controller_page_remainder_sub": "Решта знімків та відео для резервного копіювання з вибраних", "backup_controller_page_select": "Вибрати", "backup_controller_page_server_storage": "Сховище сервера", - "backup_controller_page_start_backup": "Почати Резервне Копіювання", + "backup_controller_page_start_backup": "Почати резервне копіювання", "backup_controller_page_status_off": "Автоматичне резервне копіювання в активному режимі вимкнено", "backup_controller_page_status_on": "Автоматичне резервне копіювання в активному режимі ввімкнено", "backup_controller_page_storage_format": "{} із {} спожито", @@ -137,7 +137,7 @@ "backup_manual_success": "Успіх", "backup_manual_title": "Стан завантаження", "backup_options_page_title": "Резервне копіювання", - "backup_setting_subtitle": "Manage background and foreground upload settings", + "backup_setting_subtitle": "Управління налаштуваннями завантаження у фоновому та активному режимі", "cache_settings_album_thumbnails": "Мініатюри сторінок бібліотеки ({} елементи)", "cache_settings_clear_cache_button": "Очистити кеш", "cache_settings_clear_cache_button_title": "Очищає кеш програми. Це суттєво знизить продуктивність програми, доки кеш не буде перебудовано.", @@ -156,17 +156,17 @@ "cache_settings_tile_subtitle": "Керування поведінкою локального сховища", "cache_settings_tile_title": "Локальне сховище", "cache_settings_title": "Налаштування кешування", - "cancel": "Cancel", - "canceled": "Canceled", - "change_display_order": "Change display order", + "cancel": "Скасувати", + "canceled": "Скасовано", + "change_display_order": "Змінити порядок відображення", "change_password_form_confirm_password": "Підтвердити пароль", "change_password_form_description": "Привіт {name},\n\nВи або або вперше входите у систему, або було зроблено запит на зміну вашого пароля. \nВведіть ваш новий пароль.", "change_password_form_new_password": "Новий пароль", "change_password_form_password_mismatch": "Паролі не співпадають", "change_password_form_reenter_new_password": "Повторіть новий пароль", - "check_corrupt_asset_backup": "Check for corrupt asset backups", - "check_corrupt_asset_backup_button": "Perform check", - "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", + "check_corrupt_asset_backup": "Перевірити на пошкоджені резервні копії активів", + "check_corrupt_asset_backup_button": "Виконати перевірку", + "check_corrupt_asset_backup_description": "Запустіть цю перевірку лише через Wi-Fi та після того, як всі активи будуть завантажені на сервер. Процес може зайняти кілька хвилин.", "client_cert_dialog_msg_confirm": "OK", "client_cert_enter_password": "Введіть пароль", "client_cert_import": "Імпорт", @@ -181,7 +181,7 @@ "common_create_new_album": "Створити новий альбом", "common_server_error": "Будь ласка, перевірте з'єднання, переконайтеся, що сервер доступний і версія програми/сервера сумісна.", "common_shared": "Спільні", - "completed": "Completed", + "completed": "Завершено", "contextual_search": "Схід сонця на пляжі", "control_bottom_app_bar_add_to_album": "Додати у альбом", "control_bottom_app_bar_album_info": "{} елементи", @@ -199,7 +199,7 @@ "control_bottom_app_bar_share": "Поділитися", "control_bottom_app_bar_share_to": "Поділитися", "control_bottom_app_bar_stack": "Стек", - "control_bottom_app_bar_trash_from_immich": "Перемістити до кошика", + "control_bottom_app_bar_trash_from_immich": "До кошика", "control_bottom_app_bar_unarchive": "Розархівувати", "control_bottom_app_bar_unfavorite": "Видалити з улюблених", "control_bottom_app_bar_upload": "Завантажити", @@ -213,7 +213,7 @@ "crop": "Кадрувати", "curated_location_page_title": "Місця", "curated_object_page_title": "Речі", - "current_server_address": "Current server address", + "current_server_address": "Поточна адреса сервера", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", @@ -231,7 +231,7 @@ "delete_shared_link_dialog_title": "Видалити спільне посилання", "description_input_hint_text": "Додати опис...", "description_input_submit_error": "Помилка оновлення опису, перевірте логи для подробиць", - "description_search": "Hiking day in Sapa", + "description_search": "День походу в Сапі", "download_canceled": "Завантаження скасовано", "download_complete": "Завантаження закінчено", "download_enqueue": "Завантаження поставлено в чергу", @@ -248,14 +248,14 @@ "download_sucess_android": "Медіафайли завантажено в DCIM/Immich", "download_waiting_to_retry": "Очікування повторної спроби", "edit_date_time_dialog_date_time": "Дата і час", - "edit_date_time_dialog_search_timezone": "Search timezone...", + "edit_date_time_dialog_search_timezone": "Пошук часової зони...", "edit_date_time_dialog_timezone": "Часовий пояс", "edit_image_title": "Редагувати", "edit_location_dialog_title": "Місцезнаходження", - "end_date": "End date", - "enqueued": "Enqueued", - "enter_wifi_name": "Enter WiFi name", - "error_change_sort_album": "Failed to change album sort order", + "end_date": "Дата завершення", + "enqueued": "У черзі", + "enter_wifi_name": "Введіть назву WiFi", + "error_change_sort_album": "Не вдалося змінити порядок сортування альбому", "error_saving_image": "Помилка: {}", "exif_bottom_sheet_description": "Додати опис...", "exif_bottom_sheet_details": "ПОДРОБИЦІ", @@ -267,16 +267,16 @@ "experimental_settings_new_asset_list_title": "Експериментальний макет знімків", "experimental_settings_subtitle": "На власний ризик!", "experimental_settings_title": "Експериментальні", - "external_network": "External network", - "external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom", - "failed": "Failed", + "external_network": "Зовнішня мережа", + "external_network_sheet_info": "Коли ви не підключені до переважної мережі WiFi, додаток підключатиметься до сервера через першу з наведених нижче URL-адрес, яку він зможе досягти, починаючи зверху вниз", + "failed": "Не вдалося", "favorites": "Вибране", "favorites_page_no_favorites": "Немає улюблених елементів", "favorites_page_title": "Улюблені", "filename_search": "Ім'я або розширення файлу", "filter": "Фільтр", - "get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network", - "grant_permission": "Grant permission", + "get_wifiname_error": "Не вдалося отримати назву Wi-Fi. Переконайтеся, що ви надали необхідні дозволи та підключені до Wi-Fi мережі", + "grant_permission": "Надати дозвіл", "haptic_feedback_switch": "Увімкнути тактильну віддачу", "haptic_feedback_title": "Тактильна віддача", "header_settings_add_header_tip": "Додати заголовок", @@ -322,10 +322,10 @@ "library_page_sort_most_oldest_photo": "Найдавніші фото", "library_page_sort_most_recent_photo": "Найновіші фото", "library_page_sort_title": "Назва альбому", - "local_network": "Local network", - "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", - "location_permission": "Location permission", - "location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name", + "local_network": "Локальна мережа", + "local_network_sheet_info": "Додаток підключатиметься до сервера через цей URL, коли використовується вказана Wi-Fi мережа", + "location_permission": "Дозвіл до місцезнаходження", + "location_permission_content": "Щоб перемикати мережі у фоновому режимі, Immich має *завжди* мати доступ до точної геолокації, щоб зчитувати назву Wi-Fi мережі", "location_picker_choose_on_map": "Обрати на мапі", "location_picker_latitude": "Широта", "location_picker_latitude_error": "Вкажіть дійсну широту", @@ -395,8 +395,8 @@ "multiselect_grid_edit_date_time_err_read_only": "Неможливо редагувати дату елементів лише для читання, пропущено", "multiselect_grid_edit_gps_err_read_only": "Неможливо редагувати місцезнаходження елементів лише для читання, пропущено", "my_albums": "Мої альбоми", - "networking_settings": "Networking", - "networking_subtitle": "Manage the server endpoint settings", + "networking_settings": "Мережеві налаштування", + "networking_subtitle": "Керування налаштуваннями кінцевої точки сервера", "no_assets_to_show": "Елементи відсутні", "no_name": "Без імені", "notification_permission_dialog_cancel": "Скасувати", @@ -405,7 +405,7 @@ "notification_permission_list_tile_content": "Надати дозвіл для сповіщень.", "notification_permission_list_tile_enable_button": "Увімкнути Сповіщення", "notification_permission_list_tile_title": "Дозвіл на Сповіщення", - "not_selected": "Not selected", + "not_selected": "Не вибрано", "on_this_device": "На цьому пристрої", "partner_list_user_photos": "Фотографії {user}", "partner_list_view_all": "Переглянути усі", @@ -419,7 +419,7 @@ "partner_page_stop_sharing_title": "Припинити надання ваших знімків?", "partner_page_title": "Партнер", "partners": "\nПартнери", - "paused": "Paused", + "paused": "Призупинено", "people": "Люди", "permission_onboarding_back": "Назад", "permission_onboarding_continue_anyway": "Все одно продовжити", @@ -432,7 +432,7 @@ "permission_onboarding_permission_limited": "Обмежений доступ. Аби дозволити Immich резервне копіювання та керування вашою галереєю, надайте доступ до знімків та відео у Налаштуваннях", "permission_onboarding_request": "Immich потребує доступу до ваших знімків та відео.", "places": "Місця", - "preferences_settings_subtitle": "Manage the app's preferences", + "preferences_settings_subtitle": "Керування налаштуваннями додатку", "preferences_settings_title": "Параметри", "profile_drawer_app_logs": "Журнал", "profile_drawer_client_out_of_date_major": "Мобільний додаток застарів. Будь ласка, оновіть до останньої мажорної версії.", @@ -447,7 +447,7 @@ "profile_drawer_trash": "Кошик", "recently_added": "Нещодавно додані", "recently_added_page_title": "Нещодавні", - "save": "Save", + "save": "Зберегти", "save_to_gallery": "Зберегти в галерею", "scaffold_body_error_occurred": "Виникла помилка", "search_albums": "Пошук альбому", @@ -457,17 +457,17 @@ "search_filter_camera_make": "Виробник", "search_filter_camera_model": "Модель", "search_filter_camera_title": "Виберіть тип камери", - "search_filter_contextual": "Search by context", + "search_filter_contextual": "Пошук за контекстом", "search_filter_date": "Дата", "search_filter_date_interval": "{start} до {end}", "search_filter_date_title": "Виберіть діапазон дат", - "search_filter_description": "Search by description", + "search_filter_description": "Пошук за описом", "search_filter_display_option_archive": "Архів", "search_filter_display_option_favorite": "Улюблені", "search_filter_display_option_not_in_album": "Не в альбомі", "search_filter_display_options": "Параметри відображення", "search_filter_display_options_title": "Параметри відображення", - "search_filter_filename": "Search by file name", + "search_filter_filename": "Пошук за назвою файлу", "search_filter_location": "Місцезнаходження", "search_filter_location_city": "Місто", "search_filter_location_country": "Країна", @@ -479,10 +479,10 @@ "search_filter_media_type_title": "Виберіть тип носія", "search_filter_media_type_video": "Відео", "search_filter_people": "Люди", - "search_filter_people_hint": "Filter people", + "search_filter_people_hint": "Фільтрувати за людьми", "search_filter_people_title": "Виберіть людей", - "search_no_more_result": "No more results", - "search_no_result": "No results found, try a different search term or combination", + "search_no_more_result": "Більше результатів немає", + "search_no_result": "Результатів не знайдено, спробуйте інший запит або комбінацію", "search_page_categories": "Категорії", "search_page_favorites": "Улюблені", "search_page_motion_photos": "Рухомі знімки", @@ -499,7 +499,7 @@ "search_page_places": "Місця", "search_page_recently_added": "Нещодавно додані", "search_page_screenshots": "Знімки екрану", - "search_page_search_photos_videos": "Search for your photos and videos", + "search_page_search_photos_videos": "Шукайте ваші фото та відео", "search_page_selfies": "Селфі", "search_page_things": "Речі", "search_page_videos": "Відео", @@ -512,7 +512,7 @@ "select_additional_user_for_sharing_page_suggestions": "Пропозиції", "select_user_for_sharing_page_err_album": "Не вдалося створити альбом", "select_user_for_sharing_page_share_suggestions": "Пропозиції", - "server_endpoint": "Server Endpoint", + "server_endpoint": "Кінцева точка сервера", "server_info_box_app_version": "Версія додатка", "server_info_box_latest_release": "Остання версія", "server_info_box_server_url": "URL сервера", @@ -524,7 +524,7 @@ "setting_image_viewer_preview_title": "Завантажувати зображення попереднього перегляду", "setting_image_viewer_title": "Зображення", "setting_languages_apply": "Застосувати", - "setting_languages_subtitle": "Change the app's language", + "setting_languages_subtitle": "Змінити мову додатку", "setting_languages_title": "Мова", "setting_notifications_notify_failures_grace_period": "Повідомити про помилки фонового резервного копіювання: {}", "setting_notifications_notify_hours": "{} годин", @@ -542,8 +542,8 @@ "settings_require_restart": "Перезавантажте програму для застосування цього налаштування", "setting_video_viewer_looping_subtitle": "Увімкнути циклічне відтворення відео", "setting_video_viewer_looping_title": "Циклічне відтворення", - "setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.", - "setting_video_viewer_original_video_title": "Force original video", + "setting_video_viewer_original_video_subtitle": "При трансляції відео з сервера відтворювати оригінал, навіть якщо доступна транскодування. Може призвести до буферизації. Відео, доступні локально, відтворюються в оригінальній якості, незважаючи на це налаштування.", + "setting_video_viewer_original_video_title": "Примусово відтворювати оригінальне відео", "setting_video_viewer_title": "Відео", "share_add": "Додати", "share_add_photos": "Додати знімки", @@ -562,7 +562,7 @@ "shared_album_section_people_owner_label": "Власник", "shared_album_section_people_title": "ЛЮДИ", "share_dialog_preparing": "Підготовка...", - "shared_intent_upload_button_progress_text": "{} / {} Uploaded", + "shared_intent_upload_button_progress_text": "{} / {} Завантажено", "shared_link_app_bar_title": "Спільні посилання", "shared_link_clipboard_copied_massage": "Скопійовано в буфер обміну", "shared_link_clipboard_text": "Посилання: {}\nПароль: {}", @@ -618,7 +618,7 @@ "sharing_silver_appbar_create_shared_album": "Створити спільний альбом", "sharing_silver_appbar_shared_links": "Спільні посилання", "sharing_silver_appbar_share_partner": "Поділитися з партнером", - "start_date": "Start date", + "start_date": "Дата початку", "sync": "Синхронізувати", "sync_albums": "Синхронізувати альбоми", "sync_albums_manual_subtitle": "Синхронізувати всі завантажені фото та відео у вибрані альбоми для резервного копіювання", @@ -657,15 +657,15 @@ "trash_page_select_assets_btn": "Вибрані елементи", "trash_page_select_btn": "Вибрати", "trash_page_title": "Кошик ({})", - "upload": "Upload", + "upload": "Завантажити", "upload_dialog_cancel": "Скасувати", "upload_dialog_info": "Бажаєте створити резервну копію вибраних елементів на сервері?", "upload_dialog_ok": "Завантажити", "upload_dialog_title": "Завантажити Елементи", - "uploading": "Uploading", - "upload_to_immich": "Upload to Immich ({})", - "use_current_connection": "use current connection", - "validate_endpoint_error": "Please enter a valid URL", + "uploading": "Завантаження", + "upload_to_immich": "Завантажити в Immich ({})", + "use_current_connection": "використовувати поточне підключення", + "validate_endpoint_error": "Будь ласка, введіть дійсну URL-адресу", "version_announcement_overlay_ack": "Прийняти", "version_announcement_overlay_release_notes": "примітки до випуску", "version_announcement_overlay_text_1": "Вітаємо, є новий випуск ", @@ -676,6 +676,6 @@ "viewer_remove_from_stack": "Видалити зі стеку", "viewer_stack_use_as_main_asset": "Використовувати як основний елементи", "viewer_unstack": "Розібрати стек", - "wifi_name": "WiFi Name", - "your_wifi_name": "Your WiFi name" + "wifi_name": "Назва WiFi", + "your_wifi_name": "Ваша назва WiFi" } \ No newline at end of file diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index a3db2d49a8..8e17bae9d3 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -7,8 +7,8 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/main.dart' as app; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:isar/isar.dart'; // ignore: depend_on_referenced_packages import 'package:meta/meta.dart'; @@ -39,7 +39,8 @@ class ImmichTestHelper { static Future loadApp(WidgetTester tester) async { await EasyLocalization.ensureInitialized(); // Clear all data from Isar (reuse existing instance if available) - final db = Isar.getInstance() ?? await app.loadDb(); + final db = await Bootstrap.initIsar(); + await Bootstrap.initDomain(db); await Store.clear(); await db.writeTxn(() => db.clear()); // Load main Widget diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index ab0a629ad4..1bf67ac5f9 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -541,7 +541,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 194; + CURRENT_PROJECT_VERSION = 196; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -685,7 +685,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 194; + CURRENT_PROJECT_VERSION = 196; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -715,7 +715,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 194; + CURRENT_PROJECT_VERSION = 196; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; @@ -748,7 +748,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 194; + CURRENT_PROJECT_VERSION = 196; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -791,7 +791,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 194; + CURRENT_PROJECT_VERSION = 196; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; @@ -831,7 +831,7 @@ CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 194; + CURRENT_PROJECT_VERSION = 196; CUSTOM_GROUP_ID = group.app.immich.share; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_USER_SCRIPT_SANDBOXING = YES; diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index 8f635bc61b..6c749cdb9f 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -18,13 +18,6 @@ import UIKit UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate } - do { - try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) - try AVAudioSession.sharedInstance().setActive(true) - } catch { - print("Failed to set audio session category. Error: \(error)") - } - GeneratedPluginRegistrant.register(with: self) BackgroundServicePlugin.registerBackgroundProcessing() diff --git a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift index c84b037daf..cac9faab01 100644 --- a/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift +++ b/mobile/ios/Runner/BackgroundSync/BackgroundServicePlugin.swift @@ -160,7 +160,7 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin { } } - // Called by the flutter code when enabled so that we can turn on the backround services + // Called by the flutter code when enabled so that we can turn on the background services // and save the callback information to communicate on this method channel public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) { @@ -249,7 +249,7 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin { result(true) } - // Returns the number of currently scheduled background processes to Flutter, striclty + // Returns the number of currently scheduled background processes to Flutter, strictly // for debugging func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) { BGTaskScheduler.shared.getPendingTaskRequests { requests in @@ -355,7 +355,7 @@ class BackgroundServicePlugin: NSObject, FlutterPlugin { let isExpensive = wifiMonitor.currentPath.isExpensive if (isExpensive) { // The network is expensive and we have required Wi-Fi - // Therfore, we will simply complete the task without + // Therefore, we will simply complete the task without // running it task.setTaskCompleted(success: true) return diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 3051768e53..035b0ff642 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -78,7 +78,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.126.1 + 1.128.0 CFBundleSignature ???? CFBundleURLTypes @@ -93,7 +93,7 @@ CFBundleVersion - 194 + 196 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 2f5616deb8..f9a99e76ba 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.126.1" + version_number: "1.129.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart index cc0e7ca215..868b036d1b 100644 --- a/mobile/lib/constants/constants.dart +++ b/mobile/lib/constants/constants.dart @@ -1,3 +1,6 @@ const int noDbId = -9223372036854775808; // from Isar const double downloadCompleted = -1; const double downloadFailed = -2; + +// Number of log entries to retain on app start +const int kLogTruncateLimit = 250; diff --git a/mobile/lib/constants/locales.dart b/mobile/lib/constants/locales.dart index e4e01f5b53..fa2717fcb7 100644 --- a/mobile/lib/constants/locales.dart +++ b/mobile/lib/constants/locales.dart @@ -5,7 +5,7 @@ const Map locales = { 'English (en_US)': Locale('en', 'US'), // Additional locales 'Arabic (ar_JO)': Locale('ar', 'JO'), - 'Catalan (ca_CA)': Locale('ca', 'CA'), + 'Catalan (ca)': Locale('ca'), 'Chinese (zh_CN)': Locale('zh', 'CN'), 'Chinese Simplified (zh_Hans)': Locale('zh', 'Hans'), 'Chinese TW (zh_TW)': Locale('zh', 'TW'), diff --git a/mobile/lib/domain/interfaces/log.interface.dart b/mobile/lib/domain/interfaces/log.interface.dart new file mode 100644 index 0000000000..f1cbc977dd --- /dev/null +++ b/mobile/lib/domain/interfaces/log.interface.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +import 'package:immich_mobile/domain/models/log.model.dart'; + +abstract interface class ILogRepository { + Future insert(LogMessage log); + + Future insertAll(Iterable logs); + + Future> getAll(); + + Future deleteAll(); + + /// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs + Future truncate({int limit = 250}); +} diff --git a/mobile/lib/domain/models/log.model.dart b/mobile/lib/domain/models/log.model.dart new file mode 100644 index 0000000000..dffd1cccda --- /dev/null +++ b/mobile/lib/domain/models/log.model.dart @@ -0,0 +1,65 @@ +/// Log levels according to dart logging [Level] +enum LogLevel { + all, + finest, + finer, + fine, + config, + info, + warning, + severe, + shout, + off, +} + +class LogMessage { + final String message; + final LogLevel level; + final DateTime createdAt; + final String? logger; + final String? error; + final String? stack; + + const LogMessage({ + required this.message, + required this.level, + required this.createdAt, + this.logger, + this.error, + this.stack, + }); + + @override + bool operator ==(covariant LogMessage other) { + if (identical(this, other)) return true; + + return other.message == message && + other.level == level && + other.createdAt == createdAt && + other.logger == logger && + other.error == error && + other.stack == stack; + } + + @override + int get hashCode { + return message.hashCode ^ + level.hashCode ^ + createdAt.hashCode ^ + logger.hashCode ^ + error.hashCode ^ + stack.hashCode; + } + + @override + String toString() { + return '''LogMessage: { +message: $message, +level: $level, +createdAt: $createdAt, +logger: ${logger ?? ''}, +error: ${error ?? ''}, +stack: ${stack ?? ''}, +}'''; + } +} diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart new file mode 100644 index 0000000000..2136912a67 --- /dev/null +++ b/mobile/lib/domain/services/log.service.dart @@ -0,0 +1,159 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/interfaces/log.interface.dart'; +import 'package:immich_mobile/domain/interfaces/store.interface.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:logging/logging.dart'; + +class LogService { + final ILogRepository _logRepository; + final IStoreRepository _storeRepository; + + final List _msgBuffer = []; + + /// Whether to buffer logs in memory before writing to the database. + /// This is useful when logging in quick succession, as it increases performance + /// and reduces NAND wear. However, it may cause the logs to be lost in case of a crash / in isolates. + final bool _shouldBuffer; + Timer? _flushTimer; + + late final StreamSubscription _logSubscription; + + LogService._( + this._logRepository, + this._storeRepository, + this._shouldBuffer, + ) { + // Listen to log messages and write them to the database + _logSubscription = Logger.root.onRecord.listen(_writeLogToDatabase); + } + + static LogService? _instance; + static LogService get I { + if (_instance == null) { + throw const LoggerUnInitializedException(); + } + return _instance!; + } + + static Future init({ + required ILogRepository logRepository, + required IStoreRepository storeRepository, + bool shouldBuffer = true, + }) async { + if (_instance != null) { + return _instance!; + } + _instance = await create( + logRepository: logRepository, + storeRepository: storeRepository, + shouldBuffer: shouldBuffer, + ); + return _instance!; + } + + static Future create({ + required ILogRepository logRepository, + required IStoreRepository storeRepository, + bool shouldBuffer = true, + }) async { + final instance = LogService._(logRepository, storeRepository, shouldBuffer); + // Truncate logs to 250 + await logRepository.truncate(limit: kLogTruncateLimit); + // Get log level from store + final level = await instance._storeRepository.tryGet(StoreKey.logLevel); + if (level != null) { + Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO; + } + return instance; + } + + Future setlogLevel(LogLevel level) async { + await _storeRepository.insert(StoreKey.logLevel, level.index); + Logger.root.level = level.toLevel(); + } + + Future> getMessages() async { + final logsFromDb = await _logRepository.getAll(); + if (_msgBuffer.isNotEmpty) { + return [..._msgBuffer.reversed, ...logsFromDb]; + } + return logsFromDb; + } + + Future clearLogs() async { + _flushTimer?.cancel(); + _flushTimer = null; + _msgBuffer.clear(); + await _logRepository.deleteAll(); + } + + /// Flush pending log messages to persistent storage + void flush() { + if (_flushTimer == null) { + return; + } + _flushTimer!.cancel(); + // TODO: Rename enable this after moving to sqlite - #16504 + // await _flushBufferToDatabase(); + } + + Future dispose() { + _flushTimer?.cancel(); + _logSubscription.cancel(); + return _flushBufferToDatabase(); + } + + void _writeLogToDatabase(LogRecord r) { + if (kDebugMode) { + debugPrint('[${r.level.name}] [${r.time}] ${r.message}'); + } + + final record = LogMessage( + message: r.message, + level: r.level.toLogLevel(), + createdAt: r.time, + logger: r.loggerName, + error: r.error?.toString(), + stack: r.stackTrace?.toString(), + ); + + if (_shouldBuffer) { + _msgBuffer.add(record); + _flushTimer ??= Timer( + const Duration(seconds: 5), + () => unawaited(_flushBufferToDatabase()), + ); + } else { + unawaited(_logRepository.insert(record)); + } + } + + Future _flushBufferToDatabase() async { + _flushTimer = null; + final buffer = [..._msgBuffer]; + _msgBuffer.clear(); + await _logRepository.insertAll(buffer); + } +} + +class LoggerUnInitializedException implements Exception { + const LoggerUnInitializedException(); + + @override + String toString() => 'Logger is not initialized. Call init()'; +} + +/// Log levels according to dart logging [Level] +extension LevelDomainToInfraExtension on Level { + LogLevel toLogLevel() => + LogLevel.values.elementAtOrNull(Level.LEVELS.indexOf(this)) ?? + LogLevel.info; +} + +extension on LogLevel { + Level toLevel() => Level.LEVELS.elementAtOrNull(index) ?? Level.INFO; +} diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index de79e9b71d..70b9f31c00 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -75,7 +75,7 @@ class StoreService { } /// Asynchronously stores the value in the DB and synchronously in the cache - Future put(StoreKey key, T value) async { + Future put, T>(U key, T value) async { if (_cache[key.id] == value) return; await _storeRepository.insert(key, value); _cache[key.id] = value; diff --git a/mobile/lib/entities/logger_message.entity.dart b/mobile/lib/entities/logger_message.entity.dart deleted file mode 100644 index d904e19e7a..0000000000 --- a/mobile/lib/entities/logger_message.entity.dart +++ /dev/null @@ -1,50 +0,0 @@ -// ignore_for_file: constant_identifier_names - -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; - -part 'logger_message.entity.g.dart'; - -@Collection(inheritance: false) -class LoggerMessage { - Id id = Isar.autoIncrement; - String message; - String? details; - @Enumerated(EnumType.ordinal) - LogLevel level = LogLevel.INFO; - DateTime createdAt; - String? context1; - String? context2; - - LoggerMessage({ - required this.message, - required this.details, - required this.level, - required this.createdAt, - required this.context1, - required this.context2, - }); - - @override - String toString() { - return 'InAppLoggerMessage(message: $message, level: $level, createdAt: $createdAt)'; - } -} - -/// Log levels according to dart logging [Level] -enum LogLevel { - ALL, - FINEST, - FINER, - FINE, - CONFIG, - INFO, - WARNING, - SEVERE, - SHOUT, - OFF, -} - -extension LevelExtension on Level { - LogLevel toLogLevel() => LogLevel.values[Level.LEVELS.indexOf(this)]; -} diff --git a/mobile/lib/infrastructure/entities/log.entity.dart b/mobile/lib/infrastructure/entities/log.entity.dart new file mode 100644 index 0000000000..6a38924e24 --- /dev/null +++ b/mobile/lib/infrastructure/entities/log.entity.dart @@ -0,0 +1,47 @@ +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:isar/isar.dart'; + +part 'log.entity.g.dart'; + +@Collection(inheritance: false) +class LoggerMessage { + final Id id = Isar.autoIncrement; + final String message; + final String? details; + @Enumerated(EnumType.ordinal) + final LogLevel level; + final DateTime createdAt; + final String? context1; + final String? context2; + + const LoggerMessage({ + required this.message, + required this.details, + this.level = LogLevel.info, + required this.createdAt, + required this.context1, + required this.context2, + }); + + LogMessage toDto() { + return LogMessage( + message: message, + level: level, + createdAt: createdAt, + logger: context1, + error: details, + stack: context2, + ); + } + + static LoggerMessage fromDto(LogMessage log) { + return LoggerMessage( + message: log.message, + details: log.error, + level: log.level, + createdAt: log.createdAt, + context1: log.logger, + context2: log.stack, + ); + } +} diff --git a/mobile/lib/entities/logger_message.entity.g.dart b/mobile/lib/infrastructure/entities/log.entity.g.dart similarity index 98% rename from mobile/lib/entities/logger_message.entity.g.dart rename to mobile/lib/infrastructure/entities/log.entity.g.dart index e292e7173a..9300cf15c5 100644 --- a/mobile/lib/entities/logger_message.entity.g.dart +++ b/mobile/lib/infrastructure/entities/log.entity.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'logger_message.entity.dart'; +part of 'log.entity.dart'; // ************************************************************************** // IsarCollectionGenerator @@ -117,10 +117,9 @@ LoggerMessage _loggerMessageDeserialize( createdAt: reader.readDateTime(offsets[2]), details: reader.readStringOrNull(offsets[3]), level: _LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offsets[4])] ?? - LogLevel.ALL, + LogLevel.info, message: reader.readString(offsets[5]), ); - object.id = id; return object; } @@ -141,7 +140,7 @@ P _loggerMessageDeserializeProp

( return (reader.readStringOrNull(offset)) as P; case 4: return (_LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offset)] ?? - LogLevel.ALL) as P; + LogLevel.info) as P; case 5: return (reader.readString(offset)) as P; default: @@ -150,28 +149,28 @@ P _loggerMessageDeserializeProp

( } const _LoggerMessagelevelEnumValueMap = { - 'ALL': 0, - 'FINEST': 1, - 'FINER': 2, - 'FINE': 3, - 'CONFIG': 4, - 'INFO': 5, - 'WARNING': 6, - 'SEVERE': 7, - 'SHOUT': 8, - 'OFF': 9, + 'all': 0, + 'finest': 1, + 'finer': 2, + 'fine': 3, + 'config': 4, + 'info': 5, + 'warning': 6, + 'severe': 7, + 'shout': 8, + 'off': 9, }; const _LoggerMessagelevelValueEnumMap = { - 0: LogLevel.ALL, - 1: LogLevel.FINEST, - 2: LogLevel.FINER, - 3: LogLevel.FINE, - 4: LogLevel.CONFIG, - 5: LogLevel.INFO, - 6: LogLevel.WARNING, - 7: LogLevel.SEVERE, - 8: LogLevel.SHOUT, - 9: LogLevel.OFF, + 0: LogLevel.all, + 1: LogLevel.finest, + 2: LogLevel.finer, + 3: LogLevel.fine, + 4: LogLevel.config, + 5: LogLevel.info, + 6: LogLevel.warning, + 7: LogLevel.severe, + 8: LogLevel.shout, + 9: LogLevel.off, }; Id _loggerMessageGetId(LoggerMessage object) { @@ -183,9 +182,7 @@ List> _loggerMessageGetLinks(LoggerMessage object) { } void _loggerMessageAttach( - IsarCollection col, Id id, LoggerMessage object) { - object.id = id; -} + IsarCollection col, Id id, LoggerMessage object) {} extension LoggerMessageQueryWhereSort on QueryBuilder { diff --git a/mobile/lib/infrastructure/entities/store.entity.dart b/mobile/lib/infrastructure/entities/store.entity.dart index ef47af8f52..8d6d9a7d16 100644 --- a/mobile/lib/infrastructure/entities/store.entity.dart +++ b/mobile/lib/infrastructure/entities/store.entity.dart @@ -5,8 +5,9 @@ part 'store.entity.g.dart'; /// Internal class for `Store`, do not use elsewhere. @Collection(inheritance: false) class StoreValue { - const StoreValue(this.id, {this.intValue, this.strValue}); final Id id; final int? intValue; final String? strValue; + + const StoreValue(this.id, {this.intValue, this.strValue}); } diff --git a/mobile/lib/infrastructure/repositories/log.repository.dart b/mobile/lib/infrastructure/repositories/log.repository.dart new file mode 100644 index 0000000000..6ff128f93b --- /dev/null +++ b/mobile/lib/infrastructure/repositories/log.repository.dart @@ -0,0 +1,53 @@ +import 'package:immich_mobile/domain/interfaces/log.interface.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:isar/isar.dart'; + +class IsarLogRepository extends IsarDatabaseRepository + implements ILogRepository { + final Isar _db; + const IsarLogRepository(super.db) : _db = db; + + @override + Future deleteAll() async { + await transaction(() async => await _db.loggerMessages.clear()); + return true; + } + + @override + Future> getAll() async { + final logs = + await _db.loggerMessages.where().sortByCreatedAtDesc().findAll(); + return logs.map((l) => l.toDto()).toList(); + } + + @override + Future insert(LogMessage log) async { + final logEntity = LoggerMessage.fromDto(log); + await transaction(() async { + await _db.loggerMessages.put(logEntity); + }); + return true; + } + + @override + Future insertAll(Iterable logs) async { + await transaction(() async { + final logEntities = + logs.map((log) => LoggerMessage.fromDto(log)).toList(); + await _db.loggerMessages.putAll(logEntities); + }); + return true; + } + + @override + Future truncate({int limit = 250}) async { + await transaction(() async { + final count = await _db.loggerMessages.count(); + if (count <= limit) return; + final toRemove = count - limit; + await _db.loggerMessages.where().limit(toRemove).deleteAll(); + }); + } +} diff --git a/mobile/lib/interfaces/backup.interface.dart b/mobile/lib/interfaces/backup_album.interface.dart similarity index 85% rename from mobile/lib/interfaces/backup.interface.dart rename to mobile/lib/interfaces/backup_album.interface.dart index c32199a58f..f98adb6821 100644 --- a/mobile/lib/interfaces/backup.interface.dart +++ b/mobile/lib/interfaces/backup_album.interface.dart @@ -1,7 +1,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/interfaces/database.interface.dart'; -abstract interface class IBackupRepository implements IDatabaseRepository { +abstract interface class IBackupAlbumRepository implements IDatabaseRepository { Future> getAll({BackupAlbumSort? sort}); Future> getIdsBySelection(BackupSelection backup); diff --git a/mobile/lib/interfaces/timeline.interface.dart b/mobile/lib/interfaces/timeline.interface.dart index 78b1a22111..d43f87ed5b 100644 --- a/mobile/lib/interfaces/timeline.interface.dart +++ b/mobile/lib/interfaces/timeline.interface.dart @@ -3,6 +3,10 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; abstract class ITimelineRepository { + Future> getTimelineUserIds(int id); + + Stream> watchTimelineUsers(int id); + Stream watchArchiveTimeline(int userId); Stream watchFavoriteTimeline(int userId); Stream watchTrashTimeline(int userId); diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 822d772278..407ea86d59 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -10,20 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/locales.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/album.entity.dart'; -import 'package:immich_mobile/entities/android_device_asset.entity.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; -import 'package:immich_mobile/entities/etag.entity.dart'; -import 'package:immich_mobile/entities/exif_info.entity.dart'; -import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/entities/logger_message.entity.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; @@ -33,23 +20,22 @@ import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart'; import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/services/immich_logger.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/cache/widgets_binding.dart'; import 'package:immich_mobile/utils/download.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; import 'package:immich_mobile/utils/migration.dart'; import 'package:intl/date_symbol_data_local.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:timezone/data/latest.dart'; void main() async { ImmichWidgetsBinding(); - final db = await loadDb(); + final db = await Bootstrap.initIsar(); + await Bootstrap.initDomain(db); await initApp(); await migrateDatabaseIfNeeded(db); HttpOverrides.global = HttpSSLCertOverride(); @@ -80,9 +66,6 @@ Future initApp() async { await DynamicTheme.fetchSystemPalette(); - // Initialize Immich Logger Service - ImmichLogger(); - final log = Logger("ImmichErrorLogger"); FlutterError.onError = (details) { @@ -122,29 +105,6 @@ Future initApp() async { await FileDownloader().trackTasks(); } -Future loadDb() async { - final dir = await getApplicationDocumentsDirectory(); - Isar db = await Isar.open( - [ - StoreValueSchema, - ExifInfoSchema, - AssetSchema, - AlbumSchema, - UserSchema, - BackupAlbumSchema, - DuplicatedAssetSchema, - LoggerMessageSchema, - ETagSchema, - if (Platform.isAndroid) AndroidDeviceAssetSchema, - if (Platform.isIOS) IOSDeviceAssetSchema, - ], - directory: dir.path, - maxSizeMiB: 1024, - ); - await StoreService.init(storeRepository: IsarStoreRepository(db)); - return db; -} - class ImmichApp extends ConsumerStatefulWidget { const ImmichApp({super.key}); diff --git a/mobile/lib/mixins/error_logger.mixin.dart b/mobile/lib/mixins/error_logger.mixin.dart index 9b2bc6f98e..466028c338 100644 --- a/mobile/lib/mixins/error_logger.mixin.dart +++ b/mobile/lib/mixins/error_logger.mixin.dart @@ -7,7 +7,7 @@ mixin ErrorLoggerMixin { abstract final Logger logger; /// Returns an AsyncValue if the future is successfully executed - /// Else, logs the error to the overrided logger and returns an AsyncError<> + /// Else, logs the error to the overridden logger and returns an AsyncError<> AsyncFuture guardError( Future Function() fn, { required String errorMessage, diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index d28d073b01..56c32327dd 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -2,7 +2,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/logger_message.entity.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -17,8 +18,11 @@ class AppLogPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final immichLogger = ImmichLogger(); - final logMessages = useState(immichLogger.messages); + final immichLogger = LogService.I; + final shouldReload = useState(false); + final logMessages = useFuture( + useMemoized(() => immichLogger.getMessages(), [shouldReload.value]), + ); Widget colorStatusIndicator(Color color) { return Column( @@ -37,16 +41,16 @@ class AppLogPage extends HookConsumerWidget { } Widget buildLeadingIcon(LogLevel level) => switch (level) { - LogLevel.INFO => colorStatusIndicator(context.primaryColor), - LogLevel.SEVERE => colorStatusIndicator(Colors.redAccent), - LogLevel.WARNING => colorStatusIndicator(Colors.orangeAccent), + LogLevel.info => colorStatusIndicator(context.primaryColor), + LogLevel.severe => colorStatusIndicator(Colors.redAccent), + LogLevel.warning => colorStatusIndicator(Colors.orangeAccent), _ => colorStatusIndicator(Colors.grey), }; Color getTileColor(LogLevel level) => switch (level) { - LogLevel.INFO => Colors.transparent, - LogLevel.SEVERE => Colors.redAccent.withOpacity(0.25), - LogLevel.WARNING => Colors.orangeAccent.withOpacity(0.25), + LogLevel.info => Colors.transparent, + LogLevel.severe => Colors.redAccent.withOpacity(0.25), + LogLevel.warning => Colors.orangeAccent.withOpacity(0.25), _ => context.primaryColor.withOpacity(0.1), }; @@ -71,7 +75,7 @@ class AppLogPage extends HookConsumerWidget { ), onPressed: () { immichLogger.clearLogs(); - logMessages.value = []; + shouldReload.value = !shouldReload.value; }, ), Builder( @@ -84,7 +88,7 @@ class AppLogPage extends HookConsumerWidget { size: 20.0, ), onPressed: () { - immichLogger.shareLogs(iconContext); + ImmichLogger.shareLogs(iconContext); }, ); }, @@ -105,9 +109,9 @@ class AppLogPage extends HookConsumerWidget { separatorBuilder: (context, index) { return const Divider(height: 0); }, - itemCount: logMessages.value.length, + itemCount: logMessages.data?.length ?? 0, itemBuilder: (context, index) { - var logMessage = logMessages.value[index]; + var logMessage = logMessages.data![index]; return ListTile( onTap: () => context.pushRoute( AppLogDetailRoute( @@ -128,7 +132,7 @@ class AppLogPage extends HookConsumerWidget { ), ), subtitle: Text( - "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}", + "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.logger}", style: TextStyle( fontSize: 12.0, color: context.colorScheme.onSurfaceSecondary, diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index dd6af81728..1bfea44ba1 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -1,15 +1,15 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/entities/logger_message.entity.dart'; import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; @RoutePage() class AppLogDetailPage extends HookConsumerWidget { const AppLogDetailPage({super.key, required this.logMessage}); - final LoggerMessage logMessage; + final LogMessage logMessage; @override Widget build(BuildContext context, WidgetRef ref) { @@ -126,14 +126,14 @@ class AppLogDetailPage extends HookConsumerWidget { child: ListView( children: [ buildTextWithCopyButton("MESSAGE", logMessage.message), - if (logMessage.details != null) - buildTextWithCopyButton("DETAILS", logMessage.details.toString()), - if (logMessage.context1 != null) - buildLogContext1(logMessage.context1.toString()), - if (logMessage.context2 != null) + if (logMessage.error != null) + buildTextWithCopyButton("DETAILS", logMessage.error.toString()), + if (logMessage.logger != null) + buildLogContext1(logMessage.logger.toString()), + if (logMessage.stack != null) buildTextWithCopyButton( "STACK TRACE", - logMessage.context2.toString(), + logMessage.stack.toString(), ), ], ), diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart index 7910d45e13..b3bfa366f2 100644 --- a/mobile/lib/pages/photos/photos.page.dart +++ b/mobile/lib/pages/photos/photos.page.dart @@ -110,7 +110,7 @@ class PhotosPage extends HookConsumerWidget { : const SizedBox(), renderListProvider: timelineUsers.length > 1 ? multiUsersTimelineProvider(timelineUsers) - : singleUserTimelineProvider(currentUser!.isarId), + : singleUserTimelineProvider(currentUser?.isarId), buildLoadingIndicator: buildLoadingIndicator, onRefresh: refreshAssets, stackEnabled: true, diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 780e22b818..ccd073ef07 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -1,20 +1,23 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; -import 'package:immich_mobile/services/background.service.dart'; +import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; -import 'package:immich_mobile/providers/auth.provider.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; +import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/notification_permission.provider.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; -import 'package:immich_mobile/services/immich_logger.service.dart'; +import 'package:immich_mobile/services/background.service.dart'; +import 'package:isar/isar.dart'; import 'package:permission_handler/permission_handler.dart'; enum AppLifeCycleEnum { @@ -112,11 +115,13 @@ class AppLifeCycleNotifier extends StateNotifier { _ref.read(websocketProvider.notifier).disconnect(); } - ImmichLogger().flush(); + LogService.I.flush(); } - void handleAppDetached() { + Future handleAppDetached() async { state = AppLifeCycleEnum.detached; + LogService.I.flush(); + await Isar.getInstance()?.close(); // no guarantee this is called at all _ref.read(manualUploadProvider.notifier).cancelBackup(); } diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart index 8d5209ccb7..f093d90071 100644 --- a/mobile/lib/providers/asset.provider.dart +++ b/mobile/lib/providers/asset.provider.dart @@ -59,7 +59,11 @@ class AssetNotifier extends StateNotifier { await clearAllAssets(); log.info("Manual refresh requested, cleared assets and albums from db"); } - final bool changedUsers = await _userService.refreshUsers(); + final users = await _userService.getUsersFromServer(); + bool changedUsers = false; + if (users != null) { + changedUsers = await _syncService.syncUsersFromServer(users); + } final bool newRemote = await _assetService.refreshRemoteAssets(); final bool newLocal = await _albumService.refreshDeviceAlbums(); debugPrint( diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index 68b120c38a..d699c7c763 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -104,7 +104,7 @@ class DownloadStateNotifier extends StateNotifier { } void _taskProgressCallback(TaskProgressUpdate update) { - // Ignore if the task is cancled or completed + // Ignore if the task is canceled or completed if (update.progress == -2 || update.progress == -1) { return; } diff --git a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart index a5a42ec796..ed2c485b13 100644 --- a/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart +++ b/mobile/lib/providers/asset_viewer/share_intent_upload.provider.dart @@ -117,7 +117,7 @@ class ShareIntentUploadStateNotifier } void _taskProgressCallback(TaskProgressUpdate update) { - // Ignore if the task is cancled or completed + // Ignore if the task is canceled or completed if (update.progress == downloadFailed || update.progress == downloadCompleted) { return; diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index e2b15753a9..e2939e89ce 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -47,7 +47,7 @@ class AuthNotifier extends StateNotifier { } /// Validating the url is the alternative connecting server url without - /// saving the infomation to the local database + /// saving the information to the local database Future validateAuxilaryServerUrl(String url) async { try { final validEndpoint = await _apiService.resolveEndpoint(url); diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 3b0f724411..a4f4fea45c 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; -import 'package:immich_mobile/interfaces/backup.interface.dart'; +import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; @@ -23,21 +23,34 @@ import 'package:immich_mobile/models/server_info/server_disk_info.model.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/repositories/album_media.repository.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; +import 'package:immich_mobile/services/backup_album.service.dart'; import 'package:immich_mobile/services/server_info.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart' show PMProgressHandler; +final backupProvider = + StateNotifierProvider((ref) { + return BackupNotifier( + ref.watch(backupServiceProvider), + ref.watch(serverInfoServiceProvider), + ref.watch(authProvider), + ref.watch(backgroundServiceProvider), + ref.watch(galleryPermissionNotifier.notifier), + ref.watch(albumMediaRepositoryProvider), + ref.watch(fileMediaRepositoryProvider), + ref.watch(backupAlbumServiceProvider), + ref, + ); +}); + class BackupNotifier extends StateNotifier { BackupNotifier( this._backupService, @@ -45,10 +58,9 @@ class BackupNotifier extends StateNotifier { this._authState, this._backgroundService, this._galleryPermissionNotifier, - this._db, this._albumMediaRepository, this._fileMediaRepository, - this._backupRepository, + this._backupAlbumService, this.ref, ) : super( BackUpState( @@ -96,10 +108,9 @@ class BackupNotifier extends StateNotifier { final AuthState _authState; final BackgroundService _backgroundService; final GalleryPermissionNotifier _galleryPermissionNotifier; - final Isar _db; final IAlbumMediaRepository _albumMediaRepository; final IFileMediaRepository _fileMediaRepository; - final IBackupRepository _backupRepository; + final BackupAlbumService _backupAlbumService; final Ref ref; /// @@ -260,9 +271,9 @@ class BackupNotifier extends StateNotifier { state = state.copyWith(availableAlbums: availableAlbums); final List excludedBackupAlbums = - await _backupRepository.getAllBySelection(BackupSelection.exclude); + await _backupAlbumService.getAllBySelection(BackupSelection.exclude); final List selectedBackupAlbums = - await _backupRepository.getAllBySelection(BackupSelection.select); + await _backupAlbumService.getAllBySelection(BackupSelection.select); final Set selectedAlbums = {}; for (final BackupAlbum ba in selectedBackupAlbums) { @@ -439,7 +450,7 @@ class BackupNotifier extends StateNotifier { } /// Save user selection of selected albums and excluded albums to database - Future _updatePersistentAlbumsSelection() { + Future _updatePersistentAlbumsSelection() async { final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true); final selected = state.selectedBackupAlbums.map( (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.select), @@ -447,29 +458,30 @@ class BackupNotifier extends StateNotifier { final excluded = state.excludedBackupAlbums.map( (e) => BackupAlbum(e.id, e.lastBackup ?? epoch, BackupSelection.exclude), ); - final backupAlbums = selected.followedBy(excluded).toList(); - backupAlbums.sortBy((e) => e.id); - return _db.writeTxn(() async { - final dbAlbums = await _db.backupAlbums.where().sortById().findAll(); - final List toDelete = []; - final List toUpsert = []; - // stores the most recent `lastBackup` per album but always keeps the `selection` the user just made - diffSortedListsSync( - dbAlbums, - backupAlbums, - compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), - both: (BackupAlbum a, BackupAlbum b) { - b.lastBackup = - a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; - toUpsert.add(b); - return true; - }, - onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId), - onlySecond: (BackupAlbum b) => toUpsert.add(b), - ); - await _db.backupAlbums.deleteAll(toDelete); - await _db.backupAlbums.putAll(toUpsert); - }); + final candidates = selected.followedBy(excluded).toList(); + candidates.sortBy((e) => e.id); + + final savedBackupAlbums = + await _backupAlbumService.getAll(sort: BackupAlbumSort.id); + final List toDelete = []; + final List toUpsert = []; + + diffSortedListsSync( + savedBackupAlbums, + candidates, + compare: (BackupAlbum a, BackupAlbum b) => a.id.compareTo(b.id), + both: (BackupAlbum a, BackupAlbum b) { + b.lastBackup = + a.lastBackup.isAfter(b.lastBackup) ? a.lastBackup : b.lastBackup; + toUpsert.add(b); + return true; + }, + onlyFirst: (BackupAlbum a) => toDelete.add(a.isarId), + onlySecond: (BackupAlbum b) => toUpsert.add(b), + ); + + await _backupAlbumService.deleteAll(toDelete); + await _backupAlbumService.updateAll(toUpsert); } /// Invoke backup process @@ -686,14 +698,10 @@ class BackupNotifier extends StateNotifier { } Future resumeBackup() async { - final List selectedBackupAlbums = await _db.backupAlbums - .filter() - .selectionEqualTo(BackupSelection.select) - .findAll(); - final List excludedBackupAlbums = await _db.backupAlbums - .filter() - .selectionEqualTo(BackupSelection.exclude) - .findAll(); + final List selectedBackupAlbums = + await _backupAlbumService.getAllBySelection(BackupSelection.select); + final List excludedBackupAlbums = + await _backupAlbumService.getAllBySelection(BackupSelection.exclude); Set selectedAlbums = state.selectedBackupAlbums; Set excludedAlbums = state.excludedBackupAlbums; if (selectedAlbums.isNotEmpty) { @@ -756,23 +764,8 @@ class BackupNotifier extends StateNotifier { } BackUpProgressEnum get backupProgress => state.backupProgress; + void updateBackupProgress(BackUpProgressEnum backupProgress) { state = state.copyWith(backupProgress: backupProgress); } } - -final backupProvider = - StateNotifierProvider((ref) { - return BackupNotifier( - ref.watch(backupServiceProvider), - ref.watch(serverInfoServiceProvider), - ref.watch(authProvider), - ref.watch(backgroundServiceProvider), - ref.watch(galleryPermissionNotifier.notifier), - ref.watch(dbProvider), - ref.watch(albumMediaRepositoryProvider), - ref.watch(fileMediaRepositoryProvider), - ref.watch(backupRepositoryProvider), - ref, - ); -}); diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index 192126f085..6eaf0f7226 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -9,7 +9,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/repositories/backup.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; @@ -24,6 +23,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; +import 'package:immich_mobile/services/backup_album.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; @@ -37,7 +37,7 @@ final manualUploadProvider = ref.watch(localNotificationService), ref.watch(backupProvider.notifier), ref.watch(backupServiceProvider), - ref.watch(backupRepositoryProvider), + ref.watch(backupAlbumServiceProvider), ref, ); }); @@ -47,14 +47,14 @@ class ManualUploadNotifier extends StateNotifier { final LocalNotificationService _localNotificationService; final BackupNotifier _backupProvider; final BackupService _backupService; - final BackupRepository _backupRepository; + final BackupAlbumService _backupAlbumService; final Ref ref; ManualUploadNotifier( this._localNotificationService, this._backupProvider, this._backupService, - this._backupRepository, + this._backupAlbumService, this.ref, ) : super( ManualUploadState( @@ -210,9 +210,9 @@ class ManualUploadNotifier extends StateNotifier { } final selectedBackupAlbums = - await _backupRepository.getAllBySelection(BackupSelection.select); - final excludedBackupAlbums = - await _backupRepository.getAllBySelection(BackupSelection.exclude); + await _backupAlbumService.getAllBySelection(BackupSelection.select); + final excludedBackupAlbums = await _backupAlbumService + .getAllBySelection(BackupSelection.exclude); // Get candidates from selected albums and excluded albums Set candidates = diff --git a/mobile/lib/providers/gallery_permission.provider.dart b/mobile/lib/providers/gallery_permission.provider.dart index 8077ca99fe..07d9cca591 100644 --- a/mobile/lib/providers/gallery_permission.provider.dart +++ b/mobile/lib/providers/gallery_permission.provider.dart @@ -6,7 +6,7 @@ import 'package:permission_handler/permission_handler.dart'; class GalleryPermissionNotifier extends StateNotifier { GalleryPermissionNotifier() - : super(PermissionStatus.denied) // Denied is the intitial state + : super(PermissionStatus.denied) // Denied is the initial state { // Sets the initial state getGalleryPermissionStatus(); diff --git a/mobile/lib/providers/partner.provider.dart b/mobile/lib/providers/partner.provider.dart index 38b449e96a..282e779432 100644 --- a/mobile/lib/providers/partner.provider.dart +++ b/mobile/lib/providers/partner.provider.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/entities/user.entity.dart'; class PartnerSharedWithNotifier extends StateNotifier> { final PartnerService _partnerService; + late final StreamSubscription> streamSub; PartnerSharedWithNotifier(this._partnerService) : super([]) { Function eq = const ListEquality().equals; @@ -16,7 +17,7 @@ class PartnerSharedWithNotifier extends StateNotifier> { state = partners; } }).then((_) { - _partnerService.watchSharedWith().listen((partners) { + streamSub = _partnerService.watchSharedWith().listen((partners) { if (!eq(state, partners)) { state = partners; } @@ -27,6 +28,14 @@ class PartnerSharedWithNotifier extends StateNotifier> { Future updatePartner(User partner, {required bool inTimeline}) { return _partnerService.updatePartner(partner, inTimeline: inTimeline); } + + @override + void dispose() { + if (mounted) { + streamSub.cancel(); + } + super.dispose(); + } } final partnerSharedWithProvider = @@ -38,6 +47,7 @@ final partnerSharedWithProvider = class PartnerSharedByNotifier extends StateNotifier> { final PartnerService _partnerService; + late final StreamSubscription> streamSub; PartnerSharedByNotifier(this._partnerService) : super([]) { Function eq = const ListEquality().equals; @@ -54,11 +64,11 @@ class PartnerSharedByNotifier extends StateNotifier> { }); } - late final StreamSubscription> streamSub; - @override void dispose() { - streamSub.cancel(); + if (mounted) { + streamSub.cancel(); + } super.dispose(); } } diff --git a/mobile/lib/providers/timeline.provider.dart b/mobile/lib/providers/timeline.provider.dart index b0e9482b81..97d5698c4c 100644 --- a/mobile/lib/providers/timeline.provider.dart +++ b/mobile/lib/providers/timeline.provider.dart @@ -5,8 +5,12 @@ import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/services/timeline.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; -final singleUserTimelineProvider = StreamProvider.family( +final singleUserTimelineProvider = StreamProvider.family( (ref, userId) { + if (userId == null) { + return const Stream.empty(); + } + ref.watch(localeProvider); final timelineService = ref.watch(timelineServiceProvider); return timelineService.watchHomeTimeline(userId); diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index c69245ea98..0a1bc0275a 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -5,9 +5,8 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; +import 'package:immich_mobile/services/timeline.service.dart'; class CurrentUserProvider extends StateNotifier { CurrentUserProvider(this._apiService) : super(null) { @@ -47,18 +46,15 @@ final currentUserProvider = }); class TimelineUserIdsProvider extends StateNotifier> { - TimelineUserIdsProvider(Isar db, User? currentUser) : super([]) { - final query = db.users - .filter() - .inTimelineEqualTo(true) - .or() - .isarIdEqualTo(currentUser?.isarId ?? Isar.autoIncrement) - .isarIdProperty(); - query.findAll().then((users) => state = users); - streamSub = query.watch().listen((users) => state = users); + TimelineUserIdsProvider(this._timelineService) : super([]) { + _timelineService.getTimelineUserIds().then((users) => state = users); + streamSub = _timelineService + .watchTimelineUserIds() + .listen((users) => state = users); } late final StreamSubscription> streamSub; + final TimelineService _timelineService; @override void dispose() { @@ -69,8 +65,5 @@ class TimelineUserIdsProvider extends StateNotifier> { final timelineUsersIdsProvider = StateNotifierProvider>((ref) { - return TimelineUserIdsProvider( - ref.watch(dbProvider), - ref.watch(currentUserProvider), - ); + return TimelineUserIdsProvider(ref.watch(timelineServiceProvider)); }); diff --git a/mobile/lib/repositories/backup.repository.dart b/mobile/lib/repositories/backup.repository.dart index ed3a9c27e4..f7f3051f46 100644 --- a/mobile/lib/repositories/backup.repository.dart +++ b/mobile/lib/repositories/backup.repository.dart @@ -1,15 +1,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/interfaces/backup.interface.dart'; +import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; import 'package:isar/isar.dart'; -final backupRepositoryProvider = - Provider((ref) => BackupRepository(ref.watch(dbProvider))); +final backupAlbumRepositoryProvider = + Provider((ref) => BackupAlbumRepository(ref.watch(dbProvider))); -class BackupRepository extends DatabaseRepository implements IBackupRepository { - BackupRepository(super.db); +class BackupAlbumRepository extends DatabaseRepository + implements IBackupAlbumRepository { + BackupAlbumRepository(super.db); @override Future> getAll({BackupAlbumSort? sort}) { diff --git a/mobile/lib/repositories/timeline.repository.dart b/mobile/lib/repositories/timeline.repository.dart index d1abaaf3a5..1b9ee8ad37 100644 --- a/mobile/lib/repositories/timeline.repository.dart +++ b/mobile/lib/repositories/timeline.repository.dart @@ -2,6 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/timeline.interface.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/repositories/database.repository.dart'; @@ -15,6 +16,28 @@ class TimelineRepository extends DatabaseRepository implements ITimelineRepository { TimelineRepository(super.db); + @override + Future> getTimelineUserIds(int id) { + return db.users + .filter() + .inTimelineEqualTo(true) + .or() + .isarIdEqualTo(id) + .isarIdProperty() + .findAll(); + } + + @override + Stream> watchTimelineUsers(int id) { + return db.users + .filter() + .inTimelineEqualTo(true) + .or() + .isarIdEqualTo(id) + .isarIdProperty() + .watch(); + } + @override Stream watchArchiveTimeline(int userId) { final query = db.assets diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 66a65f559e..ae5419b712 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -1,44 +1,48 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/logger_message.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart'; -import 'package:immich_mobile/pages/backup/album_preview.page.dart'; -import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; -import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; -import 'package:immich_mobile/pages/backup/backup_options.page.dart'; -import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; -import 'package:immich_mobile/pages/albums/albums.page.dart'; -import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; -import 'package:immich_mobile/pages/library/local_albums.page.dart'; -import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; -import 'package:immich_mobile/pages/library/places/places_collection.page.dart'; -import 'package:immich_mobile/pages/library/library.page.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/album/album_additional_shared_user_selection.page.dart'; import 'package:immich_mobile/pages/album/album_asset_selection.page.dart'; import 'package:immich_mobile/pages/album/album_options.page.dart'; import 'package:immich_mobile/pages/album/album_shared_user_selection.page.dart'; import 'package:immich_mobile/pages/album/album_viewer.page.dart'; +import 'package:immich_mobile/pages/albums/albums.page.dart'; +import 'package:immich_mobile/pages/backup/album_preview.page.dart'; +import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; +import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; +import 'package:immich_mobile/pages/backup/backup_options.page.dart'; +import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; +import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/app_log.page.dart'; import 'package:immich_mobile/pages/common/app_log_detail.page.dart'; import 'package:immich_mobile/pages/common/create_album.page.dart'; import 'package:immich_mobile/pages/common/gallery_viewer.page.dart'; import 'package:immich_mobile/pages/common/headers_settings.page.dart'; +import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/pages/common/tab_controller.page.dart'; -import 'package:immich_mobile/pages/editing/edit.page.dart'; import 'package:immich_mobile/pages/editing/crop.page.dart'; +import 'package:immich_mobile/pages/editing/edit.page.dart'; import 'package:immich_mobile/pages/editing/filter.page.dart'; import 'package:immich_mobile/pages/library/archive.page.dart'; import 'package:immich_mobile/pages/library/favorite.page.dart'; +import 'package:immich_mobile/pages/library/library.page.dart'; +import 'package:immich_mobile/pages/library/local_albums.page.dart'; +import 'package:immich_mobile/pages/library/partner/partner.page.dart'; +import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; +import 'package:immich_mobile/pages/library/people/people_collection.page.dart'; +import 'package:immich_mobile/pages/library/places/places_collection.page.dart'; +import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart'; +import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart'; import 'package:immich_mobile/pages/library/trash.page.dart'; import 'package:immich_mobile/pages/login/change_password.page.dart'; import 'package:immich_mobile/pages/login/login.page.dart'; @@ -54,10 +58,6 @@ import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart'; import 'package:immich_mobile/pages/search/person_result.page.dart'; import 'package:immich_mobile/pages/search/recently_added.page.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner.page.dart'; -import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart'; -import 'package:immich_mobile/pages/library/shared_link/shared_link.page.dart'; -import 'package:immich_mobile/pages/library/shared_link/shared_link_edit.page.dart'; import 'package:immich_mobile/pages/share_intent/share_intent.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index e4f1190510..299c8a602f 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -386,7 +386,7 @@ class AllVideosRoute extends PageRouteInfo { class AppLogDetailRoute extends PageRouteInfo { AppLogDetailRoute({ Key? key, - required LoggerMessage logMessage, + required LogMessage logMessage, List? children, }) : super( AppLogDetailRoute.name, @@ -419,7 +419,7 @@ class AppLogDetailRouteArgs { final Key? key; - final LoggerMessage logMessage; + final LogMessage logMessage; @override String toString() { diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 142ac48193..3a44ca7286 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -16,7 +16,7 @@ import 'package:immich_mobile/interfaces/album.interface.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; -import 'package:immich_mobile/interfaces/backup.interface.dart'; +import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; import 'package:immich_mobile/models/albums/album_search.model.dart'; import 'package:immich_mobile/repositories/album.repository.dart'; @@ -36,7 +36,7 @@ final albumServiceProvider = Provider( ref.watch(entityServiceProvider), ref.watch(albumRepositoryProvider), ref.watch(assetRepositoryProvider), - ref.watch(backupRepositoryProvider), + ref.watch(backupAlbumRepositoryProvider), ref.watch(albumMediaRepositoryProvider), ref.watch(albumApiRepositoryProvider), ), @@ -48,7 +48,7 @@ class AlbumService { final EntityService _entityService; final IAlbumRepository _albumRepository; final IAssetRepository _assetRepository; - final IBackupRepository _backupAlbumRepository; + final IBackupAlbumRepository _backupAlbumRepository; final IAlbumMediaRepository _albumMediaRepository; final IAlbumApiRepository _albumApiRepository; final Logger _log = Logger('AlbumService'); @@ -169,7 +169,10 @@ class AlbumService { final Stopwatch sw = Stopwatch()..start(); bool changes = false; try { - await _userService.refreshUsers(); + final users = await _userService.getUsersFromServer(); + if (users != null) { + await _syncService.syncUsersFromServer(users); + } final (sharedAlbum, ownedAlbum) = await ( // Note: `shared: true` is required to get albums that don't belong to // us due to unusual behaviour on the API but this will also return our diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 76701491c7..b87e10f020 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -84,15 +84,17 @@ class ApiService implements Authentication { /// port - optional (default: based on schema) /// path - optional Future resolveEndpoint(String serverUrl) async { - final url = sanitizeUrl(serverUrl); - - if (!await _isEndpointAvailable(serverUrl)) { - throw ApiException(503, "Server is not reachable"); - } + String url = sanitizeUrl(serverUrl); // Check for /.well-known/immich final wellKnownEndpoint = await _getWellKnownEndpoint(url); - if (wellKnownEndpoint.isNotEmpty) return wellKnownEndpoint; + if (wellKnownEndpoint.isNotEmpty) { + url = sanitizeUrl(wellKnownEndpoint); + } + + if (!await _isEndpointAvailable(url)) { + throw ApiException(503, "Server is not reachable"); + } // Otherwise, assume the URL provided is the api endpoint return url; @@ -128,10 +130,12 @@ class ApiService implements Authentication { var headers = {"Accept": "application/json"}; headers.addAll(getRequestHeaders()); - final res = await client.get( - Uri.parse("$baseUrl/.well-known/immich"), - headers: headers, - ); + final res = await client + .get( + Uri.parse("$baseUrl/.well-known/immich"), + headers: headers, + ) + .timeout(const Duration(seconds: 5)); if (res.statusCode == 200) { final data = jsonDecode(res.body); diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index b4a2c097b7..a4e77c216d 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/asset_api.interface.dart'; import 'package:immich_mobile/interfaces/asset_media.interface.dart'; -import 'package:immich_mobile/interfaces/backup.interface.dart'; +import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/interfaces/etag.interface.dart'; import 'package:immich_mobile/interfaces/exif_info.interface.dart'; import 'package:immich_mobile/interfaces/user.interface.dart'; @@ -39,7 +39,7 @@ final assetServiceProvider = Provider( ref.watch(exifInfoRepositoryProvider), ref.watch(userRepositoryProvider), ref.watch(etagRepositoryProvider), - ref.watch(backupRepositoryProvider), + ref.watch(backupAlbumRepositoryProvider), ref.watch(apiServiceProvider), ref.watch(syncServiceProvider), ref.watch(userServiceProvider), @@ -55,7 +55,7 @@ class AssetService { final IExifInfoRepository _exifInfoRepository; final IUserRepository _userRepository; final IETagRepository _etagRepository; - final IBackupRepository _backupRepository; + final IBackupAlbumRepository _backupRepository; final ApiService _apiService; final SyncService _syncService; final UserService _userService; diff --git a/mobile/lib/services/auth.service.dart b/mobile/lib/services/auth.service.dart index be6c64bc43..20fa62dc4b 100644 --- a/mobile/lib/services/auth.service.dart +++ b/mobile/lib/services/auth.service.dart @@ -75,7 +75,7 @@ class AuthService { isValid = true; } } catch (error) { - _log.severe("Error validating auxilary endpoint", error); + _log.severe("Error validating auxiliary endpoint", error); } finally { httpclient.close(); } @@ -187,7 +187,7 @@ class AuthService { _log.severe("Cannot resolve endpoint", error); continue; } catch (_) { - _log.severe("Auxilary server is not valid"); + _log.severe("Auxiliary server is not valid"); continue; } } diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index 81619bdca1..8183282586 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -14,8 +14,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/interfaces/backup.interface.dart'; -import 'package:immich_mobile/main.dart'; +import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; @@ -48,6 +47,7 @@ import 'package:immich_mobile/services/network.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; +import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; import 'package:network_info_plus/network_info_plus.dart'; @@ -329,7 +329,7 @@ class BackgroundService { try { _clearErrorNotifications(); - // iOS should time out after some threshhold so it doesn't wait + // iOS should time out after some threshold so it doesn't wait // indefinitely and can run later // Android is fine to wait here until the lock releases final waitForLock = Platform.isIOS @@ -369,7 +369,8 @@ class BackgroundService { } Future _onAssetsChanged() async { - final db = await loadDb(); + final db = await Bootstrap.initIsar(); + await Bootstrap.initDomain(db); HttpOverrides.global = HttpSSLCertOverride(); ApiService apiService = ApiService(); @@ -377,7 +378,7 @@ class BackgroundService { AppSettingsService settingsService = AppSettingsService(); AlbumRepository albumRepository = AlbumRepository(db); AssetRepository assetRepository = AssetRepository(db); - BackupRepository backupRepository = BackupRepository(db); + BackupAlbumRepository backupRepository = BackupAlbumRepository(db); ExifInfoRepository exifInfoRepository = ExifInfoRepository(db); ETagRepository eTagRepository = ETagRepository(db); AlbumMediaRepository albumMediaRepository = AlbumMediaRepository(); @@ -409,7 +410,6 @@ class BackgroundService { partnerApiRepository, userApiRepository, userRepository, - syncSerive, ); AlbumService albumService = AlbumService( userService, @@ -719,7 +719,6 @@ enum IosBackgroundTask { fetch, processing } /// entry point called by Kotlin/Java code; needs to be a top-level function @pragma('vm:entry-point') void _nativeEntry() { - HttpOverrides.global = HttpSSLCertOverride(); WidgetsFlutterBinding.ensureInitialized(); DartPluginRegistrant.ensureInitialized(); BackgroundService backgroundService = BackgroundService(); diff --git a/mobile/lib/services/backup_album.service.dart b/mobile/lib/services/backup_album.service.dart new file mode 100644 index 0000000000..8030d66937 --- /dev/null +++ b/mobile/lib/services/backup_album.service.dart @@ -0,0 +1,34 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/interfaces/backup_album.interface.dart'; +import 'package:immich_mobile/repositories/backup.repository.dart'; + +final backupAlbumServiceProvider = Provider((ref) { + return BackupAlbumService(ref.watch(backupAlbumRepositoryProvider)); +}); + +class BackupAlbumService { + final IBackupAlbumRepository _backupAlbumRepository; + + BackupAlbumService(this._backupAlbumRepository); + + Future> getAll({BackupAlbumSort? sort}) { + return _backupAlbumRepository.getAll(sort: sort); + } + + Future> getIdsBySelection(BackupSelection backup) { + return _backupAlbumRepository.getIdsBySelection(backup); + } + + Future> getAllBySelection(BackupSelection backup) { + return _backupAlbumRepository.getAllBySelection(backup); + } + + Future deleteAll(List ids) { + return _backupAlbumRepository.deleteAll(ids); + } + + Future updateAll(List backupAlbums) { + return _backupAlbumRepository.updateAll(backupAlbums); + } +} diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart index 5938cd7813..0d47d1a111 100644 --- a/mobile/lib/services/backup_verification.service.dart +++ b/mobile/lib/services/backup_verification.service.dart @@ -16,6 +16,7 @@ import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/repositories/exif_info.repository.dart'; import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/diff.dart'; /// Finds duplicates originating from missing EXIF information @@ -123,6 +124,8 @@ class BackupVerificationService { assert(tuple.deleteCandidates.length == tuple.originals.length); final List result = []; BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken); + final db = await Bootstrap.initIsar(); + await Bootstrap.initDomain(db); await tuple.fileMediaRepository.enableBackgroundAccess(); final ApiService apiService = ApiService(); apiService.setEndpoint(tuple.endpoint); diff --git a/mobile/lib/services/immich_logger.service.dart b/mobile/lib/services/immich_logger.service.dart index 952e8b191e..fab4b9966a 100644 --- a/mobile/lib/services/immich_logger.service.dart +++ b/mobile/lib/services/immich_logger.service.dart @@ -2,11 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/widgets.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/logger_message.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; +import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; @@ -18,75 +14,10 @@ import 'package:share_plus/share_plus.dart'; /// /// Logs can be shared by calling the `shareLogs` method, which will open a share dialog /// and generate a csv file. -class ImmichLogger { - static final ImmichLogger _instance = ImmichLogger._internal(); - final maxLogEntries = 500; - final Isar _db = Isar.getInstance()!; - List _msgBuffer = []; - Timer? _timer; +abstract final class ImmichLogger { + const ImmichLogger(); - factory ImmichLogger() => _instance; - - ImmichLogger._internal() { - _removeOverflowMessages(); - final int levelId = Store.get(StoreKey.logLevel, 5); // 5 is INFO - Logger.root.level = Level.LEVELS[levelId]; - Logger.root.onRecord.listen(_writeLogToDatabase); - } - - set level(Level level) => Logger.root.level = level; - - List get messages { - final inDb = - _db.loggerMessages.where(sort: Sort.desc).anyId().findAllSync(); - return _msgBuffer.isEmpty ? inDb : _msgBuffer.reversed.toList() + inDb; - } - - void _removeOverflowMessages() { - final msgCount = _db.loggerMessages.countSync(); - if (msgCount > maxLogEntries) { - final numberOfEntryToBeDeleted = msgCount - maxLogEntries; - _db.writeTxn( - () => _db.loggerMessages - .where() - .limit(numberOfEntryToBeDeleted) - .deleteAll(), - ); - } - } - - void _writeLogToDatabase(LogRecord record) { - debugPrint('[${record.level.name}] [${record.time}] ${record.message}'); - final lm = LoggerMessage( - message: record.message, - details: record.error?.toString(), - level: record.level.toLogLevel(), - createdAt: record.time, - context1: record.loggerName, - context2: record.stackTrace?.toString(), - ); - _msgBuffer.add(lm); - - // delayed batch writing to database: increases performance when logging - // messages in quick succession and reduces NAND wear - _timer ??= Timer(const Duration(seconds: 5), _flushBufferToDatabase); - } - - void _flushBufferToDatabase() { - _timer = null; - final buffer = _msgBuffer; - _msgBuffer = []; - _db.writeTxn(() => _db.loggerMessages.putAll(buffer)); - } - - void clearLogs() { - _timer?.cancel(); - _timer = null; - _msgBuffer.clear(); - _db.writeTxn(() => _db.loggerMessages.clear()); - } - - Future shareLogs(BuildContext context) async { + static Future shareLogs(BuildContext context) async { final tempDir = await getTemporaryDirectory(); final dateTime = DateTime.now().toIso8601String(); final filePath = '${tempDir.path}/Immich_log_$dateTime.log'; @@ -94,13 +25,13 @@ class ImmichLogger { final io = logFile.openWrite(); try { // Write messages - for (final m in messages) { + for (final m in await LogService.I.getMessages()) { final created = m.createdAt; final level = m.level.name.padRight(8); - final logger = (m.context1 ?? "").padRight(20); + final logger = (m.logger ?? "").padRight(20); final message = m.message; - final error = m.details != null ? " ${m.details} |" : ""; - final stack = m.context2 != null ? "\n${m.context2!}" : ""; + final error = m.error == null ? "" : " ${m.error} |"; + final stack = m.stack == null ? "" : "\n${m.stack!}"; io.write('$created | $level | $logger | $message |$error$stack\n'); } } finally { @@ -115,16 +46,6 @@ class ImmichLogger { [XFile(filePath)], subject: "Immich logs $dateTime", sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, - ).then( - (value) => logFile.delete(), - ); - } - - /// Flush pending log messages to persistent storage - void flush() { - if (_timer != null) { - _timer!.cancel(); - _db.writeTxnSync(() => _db.loggerMessages.putAllSync(_msgBuffer)); - } + ).then((value) => logFile.delete()); } } diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart index 3afb6d78d3..6ae8e1d0bb 100644 --- a/mobile/lib/services/memory.service.dart +++ b/mobile/lib/services/memory.service.dart @@ -26,7 +26,7 @@ class MemoryService { try { final now = DateTime.now(); final data = await _apiService.memoriesApi.searchMemories( - for_: now, + for_: DateTime.utc(now.year, now.month, now.day, 0, 0, 0), ); if (data == null) { diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index ddca266006..d01a49a38d 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/interfaces/album.interface.dart'; import 'package:immich_mobile/interfaces/album_api.interface.dart'; import 'package:immich_mobile/interfaces/album_media.interface.dart'; @@ -23,7 +24,6 @@ import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; -import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/utils/datetime_comparison.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; @@ -639,7 +639,7 @@ class SyncService { } /// fast path for common case: only new assets were added to device album - /// returns `true` if successfull, else `false` + /// returns `true` if successful, else `false` Future _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async { if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) { return false; diff --git a/mobile/lib/services/timeline.service.dart b/mobile/lib/services/timeline.service.dart index a89377548f..db85230662 100644 --- a/mobile/lib/services/timeline.service.dart +++ b/mobile/lib/services/timeline.service.dart @@ -21,12 +21,23 @@ class TimelineService { final ITimelineRepository _timelineRepository; final IUserRepository _userRepository; final AppSettingsService _appSettingsService; - TimelineService( + + const TimelineService( this._timelineRepository, this._userRepository, this._appSettingsService, ); + Future> getTimelineUserIds() async { + final me = await _userRepository.me(); + return _timelineRepository.getTimelineUserIds(me.isarId); + } + + Stream> watchTimelineUserIds() async* { + final me = await _userRepository.me(); + yield* _timelineRepository.watchTimelineUsers(me.isarId); + } + Stream watchHomeTimeline(int userId) { return _timelineRepository.watchHomeTimeline(userId, _getGroupByOption()); } diff --git a/mobile/lib/services/user.service.dart b/mobile/lib/services/user.service.dart index 935a751e2a..921202ec59 100644 --- a/mobile/lib/services/user.service.dart +++ b/mobile/lib/services/user.service.dart @@ -1,14 +1,13 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/interfaces/partner_api.interface.dart'; import 'package:immich_mobile/interfaces/user.interface.dart'; import 'package:immich_mobile/interfaces/user_api.interface.dart'; import 'package:immich_mobile/repositories/partner_api.repository.dart'; import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:immich_mobile/repositories/user_api.repository.dart'; -import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; @@ -17,7 +16,6 @@ final userServiceProvider = Provider( ref.watch(partnerApiRepositoryProvider), ref.watch(userApiRepositoryProvider), ref.watch(userRepositoryProvider), - ref.watch(syncServiceProvider), ), ); @@ -25,14 +23,12 @@ class UserService { final IPartnerApiRepository _partnerApiRepository; final IUserApiRepository _userApiRepository; final IUserRepository _userRepository; - final SyncService _syncService; final Logger _log = Logger("UserService"); UserService( this._partnerApiRepository, this._userApiRepository, this._userRepository, - this._syncService, ); Future> getUsers({bool self = false}) { @@ -98,12 +94,6 @@ class UserService { return users; } - Future refreshUsers() async { - final users = await getUsersFromServer(); - if (users == null) return false; - return _syncService.syncUsersFromServer(users); - } - Future clearTable() { return _userRepository.clearTable(); } diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart new file mode 100644 index 0000000000..4a9ce1a5e1 --- /dev/null +++ b/mobile/lib/utils/bootstrap.dart @@ -0,0 +1,56 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/domain/services/log.service.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/android_device_asset.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; +import 'package:immich_mobile/entities/etag.entity.dart'; +import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:isar/isar.dart'; +import 'package:path_provider/path_provider.dart'; + +abstract final class Bootstrap { + static Future initIsar() async { + if (Isar.getInstance() != null) { + return Isar.getInstance()!; + } + + final dir = await getApplicationDocumentsDirectory(); + return await Isar.open( + [ + StoreValueSchema, + ExifInfoSchema, + AssetSchema, + AlbumSchema, + UserSchema, + BackupAlbumSchema, + DuplicatedAssetSchema, + LoggerMessageSchema, + ETagSchema, + if (Platform.isAndroid) AndroidDeviceAssetSchema, + if (Platform.isIOS) IOSDeviceAssetSchema, + ], + directory: dir.path, + maxSizeMiB: 1024, + inspector: kDebugMode, + ); + } + + static Future initDomain(Isar db) async { + await StoreService.init(storeRepository: IsarStoreRepository(db)); + await LogService.init( + logRepository: IsarLogRepository(db), + storeRepository: IsarStoreRepository(db), + ); + } +} diff --git a/mobile/lib/utils/cache/custom_image_cache.dart b/mobile/lib/utils/cache/custom_image_cache.dart index a2a7839172..dcb8dacb0d 100644 --- a/mobile/lib/utils/cache/custom_image_cache.dart +++ b/mobile/lib/utils/cache/custom_image_cache.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart' import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart'; /// [ImageCache] that uses two caches for small and large images -/// so that a single large image does not evict all small iamges +/// so that a single large image does not evict all small images final class CustomImageCache implements ImageCache { final _small = ImageCache(); final _large = ImageCache()..maximumSize = 5; // Maximum 5 images diff --git a/mobile/lib/widgets/photo_view/src/utils/photo_view_utils.dart b/mobile/lib/widgets/photo_view/src/utils/photo_view_utils.dart index 9c632df3bf..facd701725 100644 --- a/mobile/lib/widgets/photo_view/src/utils/photo_view_utils.dart +++ b/mobile/lib/widgets/photo_view/src/utils/photo_view_utils.dart @@ -26,7 +26,7 @@ double getScaleForScaleState( } /// Internal class to wraps custom scale boundaries (min, max and initial) -/// Also, stores values regarding the two sizes: the container and teh child. +/// Also, stores values regarding the two sizes: the container and the child. class ScaleBoundaries { const ScaleBoundaries( this._minScale, diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index ec1ab79cf7..4e399e8aec 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -1,18 +1,19 @@ import 'dart:io'; + import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/log.service.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; +import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; import 'package:immich_mobile/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart'; import 'package:immich_mobile/widgets/settings/local_storage_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/immich_logger.service.dart'; -import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; import 'package:immich_mobile/widgets/settings/ssl_client_cert_settings.dart'; import 'package:logging/logging.dart'; @@ -33,7 +34,8 @@ class AdvancedSettings extends HookConsumerWidget { useValueChanged( levelId.value, - (_, __) => ImmichLogger().level = Level.LEVELS[levelId.value], + (_, __) => + LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()), ); final advancedSettings = [ diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index 1089029947..587a0ce6d3 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -2,13 +2,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/network.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; +import 'package:immich_mobile/utils/url_helper.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/external_network_preference.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/local_network_preference.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -18,7 +17,7 @@ class NetworkingSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentEndpoint = Store.get(StoreKey.serverEndpoint); + final currentEndpoint = getServerUrl(); final featureEnabled = useAppSettingsState(AppSettingsEnum.autoEndpointSwitching); @@ -102,7 +101,7 @@ class NetworkingSettings extends HookConsumerWidget { padding: const EdgeInsets.only(top: 8, left: 16, bottom: 8), child: NetworkPreferenceTitle( title: "current_server_address".tr().toUpperCase(), - icon: currentEndpoint.startsWith('https') + icon: (currentEndpoint?.startsWith('https') ?? false) ? Icons.https_outlined : Icons.http_outlined, ), @@ -119,10 +118,16 @@ class NetworkingSettings extends HookConsumerWidget { ), ), child: ListTile( - leading: - const Icon(Icons.check_circle_rounded, color: Colors.green), + leading: currentEndpoint != null + ? const Icon( + Icons.check_circle_rounded, + color: Colors.green, + ) + : const Icon( + Icons.circle_outlined, + ), title: Text( - currentEndpoint, + currentEndpoint ?? "--", style: TextStyle( fontSize: 16, fontFamily: 'Inconsolata', diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 80d85bac9a..e3f6a74856 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.126.1 +- API version: 1.129.0 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -425,6 +425,8 @@ Class | Method | HTTP request | Description - [SyncAckDto](doc//SyncAckDto.md) - [SyncAckSetDto](doc//SyncAckSetDto.md) - [SyncEntityType](doc//SyncEntityType.md) + - [SyncPartnerDeleteV1](doc//SyncPartnerDeleteV1.md) + - [SyncPartnerV1](doc//SyncPartnerV1.md) - [SyncRequestType](doc//SyncRequestType.md) - [SyncStreamDto](doc//SyncStreamDto.md) - [SyncUserDeleteV1](doc//SyncUserDeleteV1.md) diff --git a/mobile/openapi/devtools_options.yaml b/mobile/openapi/devtools_options.yaml deleted file mode 100644 index fa0b357c4f..0000000000 --- a/mobile/openapi/devtools_options.yaml +++ /dev/null @@ -1,3 +0,0 @@ -description: This file stores settings for Dart & Flutter DevTools. -documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states -extensions: diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 893587e7fc..04dc43f88c 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -232,6 +232,8 @@ part 'model/sync_ack_delete_dto.dart'; part 'model/sync_ack_dto.dart'; part 'model/sync_ack_set_dto.dart'; part 'model/sync_entity_type.dart'; +part 'model/sync_partner_delete_v1.dart'; +part 'model/sync_partner_v1.dart'; part 'model/sync_request_type.dart'; part 'model/sync_stream_dto.dart'; part 'model/sync_user_delete_v1.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 7c2dc53455..4d837ccb9d 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -520,6 +520,10 @@ class ApiClient { return SyncAckSetDto.fromJson(value); case 'SyncEntityType': return SyncEntityTypeTypeTransformer().decode(value); + case 'SyncPartnerDeleteV1': + return SyncPartnerDeleteV1.fromJson(value); + case 'SyncPartnerV1': + return SyncPartnerV1.fromJson(value); case 'SyncRequestType': return SyncRequestTypeTypeTransformer().decode(value); case 'SyncStreamDto': diff --git a/mobile/openapi/lib/model/source_type.dart b/mobile/openapi/lib/model/source_type.dart index 13c450b010..4da5aba495 100644 --- a/mobile/openapi/lib/model/source_type.dart +++ b/mobile/openapi/lib/model/source_type.dart @@ -25,11 +25,13 @@ class SourceType { static const machineLearning = SourceType._(r'machine-learning'); static const exif = SourceType._(r'exif'); + static const manual = SourceType._(r'manual'); /// List of all possible values in this [enum][SourceType]. static const values = [ machineLearning, exif, + manual, ]; static SourceType? fromJson(dynamic value) => SourceTypeTypeTransformer().decode(value); @@ -70,6 +72,7 @@ class SourceTypeTypeTransformer { switch (data) { case r'machine-learning': return SourceType.machineLearning; case r'exif': return SourceType.exif; + case r'manual': return SourceType.manual; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/sync_entity_type.dart b/mobile/openapi/lib/model/sync_entity_type.dart index ed82205a37..5d130f7f93 100644 --- a/mobile/openapi/lib/model/sync_entity_type.dart +++ b/mobile/openapi/lib/model/sync_entity_type.dart @@ -25,11 +25,15 @@ class SyncEntityType { static const userV1 = SyncEntityType._(r'UserV1'); static const userDeleteV1 = SyncEntityType._(r'UserDeleteV1'); + static const partnerV1 = SyncEntityType._(r'PartnerV1'); + static const partnerDeleteV1 = SyncEntityType._(r'PartnerDeleteV1'); /// List of all possible values in this [enum][SyncEntityType]. static const values = [ userV1, userDeleteV1, + partnerV1, + partnerDeleteV1, ]; static SyncEntityType? fromJson(dynamic value) => SyncEntityTypeTypeTransformer().decode(value); @@ -70,6 +74,8 @@ class SyncEntityTypeTypeTransformer { switch (data) { case r'UserV1': return SyncEntityType.userV1; case r'UserDeleteV1': return SyncEntityType.userDeleteV1; + case r'PartnerV1': return SyncEntityType.partnerV1; + case r'PartnerDeleteV1': return SyncEntityType.partnerDeleteV1; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/openapi/lib/model/sync_partner_delete_v1.dart b/mobile/openapi/lib/model/sync_partner_delete_v1.dart new file mode 100644 index 0000000000..f5e10d6576 --- /dev/null +++ b/mobile/openapi/lib/model/sync_partner_delete_v1.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncPartnerDeleteV1 { + /// Returns a new [SyncPartnerDeleteV1] instance. + SyncPartnerDeleteV1({ + required this.sharedById, + required this.sharedWithId, + }); + + String sharedById; + + String sharedWithId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncPartnerDeleteV1 && + other.sharedById == sharedById && + other.sharedWithId == sharedWithId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (sharedById.hashCode) + + (sharedWithId.hashCode); + + @override + String toString() => 'SyncPartnerDeleteV1[sharedById=$sharedById, sharedWithId=$sharedWithId]'; + + Map toJson() { + final json = {}; + json[r'sharedById'] = this.sharedById; + json[r'sharedWithId'] = this.sharedWithId; + return json; + } + + /// Returns a new [SyncPartnerDeleteV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncPartnerDeleteV1? fromJson(dynamic value) { + upgradeDto(value, "SyncPartnerDeleteV1"); + if (value is Map) { + final json = value.cast(); + + return SyncPartnerDeleteV1( + sharedById: mapValueOfType(json, r'sharedById')!, + sharedWithId: mapValueOfType(json, r'sharedWithId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncPartnerDeleteV1.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SyncPartnerDeleteV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncPartnerDeleteV1-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SyncPartnerDeleteV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'sharedById', + 'sharedWithId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_partner_v1.dart b/mobile/openapi/lib/model/sync_partner_v1.dart new file mode 100644 index 0000000000..e551c4c83d --- /dev/null +++ b/mobile/openapi/lib/model/sync_partner_v1.dart @@ -0,0 +1,115 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SyncPartnerV1 { + /// Returns a new [SyncPartnerV1] instance. + SyncPartnerV1({ + required this.inTimeline, + required this.sharedById, + required this.sharedWithId, + }); + + bool inTimeline; + + String sharedById; + + String sharedWithId; + + @override + bool operator ==(Object other) => identical(this, other) || other is SyncPartnerV1 && + other.inTimeline == inTimeline && + other.sharedById == sharedById && + other.sharedWithId == sharedWithId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (inTimeline.hashCode) + + (sharedById.hashCode) + + (sharedWithId.hashCode); + + @override + String toString() => 'SyncPartnerV1[inTimeline=$inTimeline, sharedById=$sharedById, sharedWithId=$sharedWithId]'; + + Map toJson() { + final json = {}; + json[r'inTimeline'] = this.inTimeline; + json[r'sharedById'] = this.sharedById; + json[r'sharedWithId'] = this.sharedWithId; + return json; + } + + /// Returns a new [SyncPartnerV1] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SyncPartnerV1? fromJson(dynamic value) { + upgradeDto(value, "SyncPartnerV1"); + if (value is Map) { + final json = value.cast(); + + return SyncPartnerV1( + inTimeline: mapValueOfType(json, r'inTimeline')!, + sharedById: mapValueOfType(json, r'sharedById')!, + sharedWithId: mapValueOfType(json, r'sharedWithId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SyncPartnerV1.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SyncPartnerV1.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SyncPartnerV1-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SyncPartnerV1.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'inTimeline', + 'sharedById', + 'sharedWithId', + }; +} + diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index d7f1bde54c..c35b17dea1 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -24,10 +24,12 @@ class SyncRequestType { String toJson() => value; static const usersV1 = SyncRequestType._(r'UsersV1'); + static const partnersV1 = SyncRequestType._(r'PartnersV1'); /// List of all possible values in this [enum][SyncRequestType]. static const values = [ usersV1, + partnersV1, ]; static SyncRequestType? fromJson(dynamic value) => SyncRequestTypeTypeTransformer().decode(value); @@ -67,6 +69,7 @@ class SyncRequestTypeTypeTransformer { if (data != null) { switch (data) { case r'UsersV1': return SyncRequestType.usersV1; + case r'PartnersV1': return SyncRequestType.partnersV1; default: if (!allowNull) { throw ArgumentError('Unknown enum value to decode: $data'); diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 5a15bf5f5e..fc3105098f 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -394,10 +394,10 @@ packages: dependency: "direct main" description: name: easy_localization - sha256: fa59bcdbbb911a764aa6acf96bbb6fa7a5cf8234354fc45ec1a43a0349ef0201 + sha256: "0f5239c7b8ab06c66440cfb0e9aa4b4640429c6668d5a42fe389c5de42220b12" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.0.7+1" easy_logger: dependency: transitive description: @@ -407,7 +407,7 @@ packages: source: hosted version: "0.0.2" fake_async: - dependency: transitive + dependency: "direct dev" description: name: fake_async sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" @@ -1012,8 +1012,8 @@ packages: dependency: "direct main" description: path: "." - ref: "4530808" - resolved-ref: "4530808a6d04c9992de184c423c9e87fbf6a53eb" + ref: "5459d54" + resolved-ref: "5459d54cdc1cf4d99e2193b310052f1ebb5dcf43" url: "https://github.com/immich-app/native_video_player" source: git version: "1.3.1" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index dd046a60eb..f5e4b05d49 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.126.1+184 +version: 1.129.0+187 environment: sdk: '>=3.3.0 <4.0.0' @@ -34,7 +34,7 @@ dependencies: url_launcher: ^6.2.4 http: ^1.1.0 cancellation_token_http: ^2.0.0 - easy_localization: ^3.0.3 + easy_localization: ^3.0.7+1 share_plus: ^10.0.0 flutter_displaymode: ^0.6.0 scrollable_positioned_list: ^0.3.8 @@ -65,7 +65,7 @@ dependencies: native_video_player: git: url: https://github.com/immich-app/native_video_player - ref: '4530808' + ref: '5459d54' #image editing packages crop_image: ^1.0.13 @@ -113,6 +113,7 @@ dev_dependencies: mocktail: ^1.0.3 immich_mobile_immich_lint: path: './immich_lint' + fake_async: ^1.3.1 flutter: uses-material-design: true diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart new file mode 100644 index 0000000000..5811a8c430 --- /dev/null +++ b/mobile/test/domain/services/log_service_test.dart @@ -0,0 +1,188 @@ +import 'package:collection/collection.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/interfaces/log.interface.dart'; +import 'package:immich_mobile/domain/interfaces/store.interface.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/log.service.dart'; +import 'package:logging/logging.dart'; +import 'package:mocktail/mocktail.dart'; + +import '../../infrastructure/repository.mock.dart'; +import '../../test_utils.dart'; + +final _kInfoLog = LogMessage( + message: '#Info Message', + level: LogLevel.info, + createdAt: DateTime(2025, 2, 26), + logger: 'Info Logger', +); + +final _kWarnLog = LogMessage( + message: '#Warn Message', + level: LogLevel.warning, + createdAt: DateTime(2025, 2, 27), + logger: 'Warn Logger', +); + +void main() { + late LogService sut; + late ILogRepository mockLogRepo; + late IStoreRepository mockStoreRepo; + + setUp(() async { + mockLogRepo = MockLogRepository(); + mockStoreRepo = MockStoreRepository(); + + registerFallbackValue(_kInfoLog); + + when(() => mockLogRepo.truncate(limit: any(named: 'limit'))) + .thenAnswer((_) async => {}); + when(() => mockStoreRepo.tryGet(StoreKey.logLevel)) + .thenAnswer((_) async => LogLevel.fine.index); + when(() => mockLogRepo.getAll()).thenAnswer((_) async => []); + when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true); + when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true); + + sut = await LogService.create( + logRepository: mockLogRepo, + storeRepository: mockStoreRepo, + ); + }); + + tearDown(() async { + await sut.dispose(); + }); + + group("Log Service Init:", () { + test('Truncates the existing logs on init', () { + final limit = + verify(() => mockLogRepo.truncate(limit: captureAny(named: 'limit'))) + .captured + .firstOrNull as int?; + expect(limit, kLogTruncateLimit); + }); + + test('Sets log level based on the store setting', () { + verify(() => mockStoreRepo.tryGet(StoreKey.logLevel)).called(1); + expect(Logger.root.level, Level.FINE); + }); + }); + + group("Log Service Set Level:", () { + setUp(() async { + when(() => mockStoreRepo.insert(StoreKey.logLevel, any())) + .thenAnswer((_) async => true); + await sut.setlogLevel(LogLevel.shout); + }); + + test('Updates the log level in store', () { + final index = verify( + () => mockStoreRepo.insert(StoreKey.logLevel, captureAny()), + ).captured.firstOrNull; + expect(index, LogLevel.shout.index); + }); + + test('Sets log level on logger', () { + expect(Logger.root.level, Level.SHOUT); + }); + }); + + group("Log Service Buffer:", () { + test('Buffers logs until timer elapses', () { + TestUtils.fakeAsync((time) async { + sut = await LogService.create( + logRepository: mockLogRepo, + storeRepository: mockStoreRepo, + shouldBuffer: true, + ); + + final logger = Logger(_kInfoLog.logger!); + logger.info(_kInfoLog.message); + expect(await sut.getMessages(), hasLength(1)); + logger.warning(_kWarnLog.message); + expect(await sut.getMessages(), hasLength(2)); + time.elapse(const Duration(seconds: 6)); + expect(await sut.getMessages(), isEmpty); + }); + }); + + test('Batch inserts all logs on timer', () { + TestUtils.fakeAsync((time) async { + sut = await LogService.create( + logRepository: mockLogRepo, + storeRepository: mockStoreRepo, + shouldBuffer: true, + ); + + final logger = Logger(_kInfoLog.logger!); + logger.info(_kInfoLog.message); + time.elapse(const Duration(seconds: 6)); + final insert = verify(() => mockLogRepo.insertAll(captureAny())); + insert.called(1); + // ignore: prefer-correct-json-casts + final captured = insert.captured.firstOrNull as List; + expect(captured.firstOrNull?.message, _kInfoLog.message); + expect(captured.firstOrNull?.logger, _kInfoLog.logger); + + verifyNever(() => mockLogRepo.insert(captureAny())); + }); + }); + + test('Does not buffer when off', () { + TestUtils.fakeAsync((time) async { + sut = await LogService.create( + logRepository: mockLogRepo, + storeRepository: mockStoreRepo, + shouldBuffer: false, + ); + + final logger = Logger(_kInfoLog.logger!); + logger.info(_kInfoLog.message); + // Ensure nothing gets buffer. This works because we mock log repo getAll to return nothing + expect(await sut.getMessages(), isEmpty); + + final insert = verify(() => mockLogRepo.insert(captureAny())); + insert.called(1); + final captured = insert.captured.firstOrNull as LogMessage; + expect(captured.message, _kInfoLog.message); + expect(captured.logger, _kInfoLog.logger); + + verifyNever(() => mockLogRepo.insertAll(captureAny())); + }); + }); + }); + + group("Log Service Get messages:", () { + setUp(() { + when(() => mockLogRepo.getAll()).thenAnswer((_) async => [_kInfoLog]); + }); + + test('Fetches result from DB', () async { + expect(await sut.getMessages(), hasLength(1)); + verify(() => mockLogRepo.getAll()).called(1); + }); + + test('Combines result from both DB + Buffer', () { + TestUtils.fakeAsync((time) async { + sut = await LogService.create( + logRepository: mockLogRepo, + storeRepository: mockStoreRepo, + shouldBuffer: true, + ); + + final logger = Logger(_kWarnLog.logger!); + logger.warning(_kWarnLog.message); + expect(await sut.getMessages(), hasLength(2)); // 1 - DB, 1 - Buff + + final messages = await sut.getMessages(); + // Logged time is assigned in the service for messages in the buffer, so compare manually + expect(messages.firstOrNull?.message, _kWarnLog.message); + expect(messages.firstOrNull?.logger, _kWarnLog.logger); + + expect(messages.elementAtOrNull(1), _kInfoLog); + }); + }); + }); +} diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index ff25bdac9d..3e33fdac0a 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -1,4 +1,7 @@ +import 'package:immich_mobile/domain/interfaces/log.interface.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:mocktail/mocktail.dart'; class MockStoreRepository extends Mock implements IStoreRepository {} + +class MockLogRepository extends Mock implements ILogRepository {} diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index 464dafc82b..a58de21613 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -1,15 +1,16 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/user.interface.dart'; -import 'package:immich_mobile/services/immich_logger.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -70,7 +71,10 @@ void main() { db.writeTxnSync(() => db.clearSync()); await StoreService.init(storeRepository: IsarStoreRepository(db)); await Store.put(StoreKey.currentUser, owner); - ImmichLogger(); + await LogService.init( + logRepository: IsarLogRepository(db), + storeRepository: IsarStoreRepository(db), + ); }); final List initialAssets = [ makeAsset(checksum: "a", remoteId: "0-1"), diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart index 3dda932cac..bad7d3ebab 100644 --- a/mobile/test/repository.mocks.dart +++ b/mobile/test/repository.mocks.dart @@ -5,7 +5,7 @@ import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/asset_media.interface.dart'; import 'package:immich_mobile/interfaces/auth.interface.dart'; import 'package:immich_mobile/interfaces/auth_api.interface.dart'; -import 'package:immich_mobile/interfaces/backup.interface.dart'; +import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/interfaces/etag.interface.dart'; import 'package:immich_mobile/interfaces/exif_info.interface.dart'; import 'package:immich_mobile/interfaces/file_media.interface.dart'; @@ -18,7 +18,7 @@ class MockAssetRepository extends Mock implements IAssetRepository {} class MockUserRepository extends Mock implements IUserRepository {} -class MockBackupRepository extends Mock implements IBackupRepository {} +class MockBackupRepository extends Mock implements IBackupAlbumRepository {} class MockExifInfoRepository extends Mock implements IExifInfoRepository {} diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart index c0775a1c3e..983b355dcb 100644 --- a/mobile/test/services/album.service_test.dart +++ b/mobile/test/services/album.service_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:mocktail/mocktail.dart'; + import '../fixtures/album.stub.dart'; import '../fixtures/asset.stub.dart'; import '../fixtures/user.stub.dart'; @@ -83,7 +84,9 @@ void main() { group('refreshRemoteAlbums', () { test('is working', () async { - when(() => userService.refreshUsers()).thenAnswer((_) async => true); + when(() => userService.getUsersFromServer()).thenAnswer((_) async => []); + when(() => syncService.syncUsersFromServer(any())) + .thenAnswer((_) async => true); when(() => albumApiRepository.getAll(shared: true)) .thenAnswer((_) async => [AlbumStub.sharedWithUser]); @@ -99,7 +102,8 @@ void main() { ).thenAnswer((_) async => true); final result = await sut.refreshRemoteAlbums(); expect(result, true); - verify(() => userService.refreshUsers()).called(1); + verify(() => userService.getUsersFromServer()).called(1); + verify(() => syncService.syncUsersFromServer([])).called(1); verify(() => albumApiRepository.getAll(shared: true)).called(1); verify(() => albumApiRepository.getAll(shared: null)).called(1); verify( diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 35ab1fb0aa..825d77190b 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -1,6 +1,8 @@ +import 'dart:async'; import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; +import 'package:fake_async/fake_async.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/album.entity.dart'; @@ -11,8 +13,8 @@ import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/entities/logger_message.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/log.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; @@ -88,4 +90,36 @@ abstract final class TestUtils { WidgetController.hitTestWarningShouldBeFatal = true; HttpOverrides.global = MockHttpOverrides(); } + + // Workaround till the following issue is resolved + // https://github.com/dart-lang/test/issues/2307 + static T fakeAsync( + Future Function(FakeAsync _) callback, { + DateTime? initialTime, + }) { + late final T result; + Object? error; + StackTrace? stack; + FakeAsync(initialTime: initialTime).run((FakeAsync async) { + bool shouldPump = true; + unawaited( + callback(async).then( + (value) => result = value, + onError: (e, s) { + error = e; + stack = s; + }, + ).whenComplete(() => shouldPump = false), + ); + + while (shouldPump) { + async.flushMicrotasks(); + } + }); + + if (error != null) { + Error.throwWithStackTrace(error!, stack!); + } + return result; + } } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index e8bdfa7405..c1921da82d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7655,7 +7655,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.126.1", + "version": "1.129.0", "contact": {} }, "tags": [], @@ -11952,7 +11952,8 @@ "SourceType": { "enum": [ "machine-learning", - "exif" + "exif", + "manual" ], "type": "string" }, @@ -12051,13 +12052,50 @@ "SyncEntityType": { "enum": [ "UserV1", - "UserDeleteV1" + "UserDeleteV1", + "PartnerV1", + "PartnerDeleteV1" ], "type": "string" }, + "SyncPartnerDeleteV1": { + "properties": { + "sharedById": { + "type": "string" + }, + "sharedWithId": { + "type": "string" + } + }, + "required": [ + "sharedById", + "sharedWithId" + ], + "type": "object" + }, + "SyncPartnerV1": { + "properties": { + "inTimeline": { + "type": "boolean" + }, + "sharedById": { + "type": "string" + }, + "sharedWithId": { + "type": "string" + } + }, + "required": [ + "inTimeline", + "sharedById", + "sharedWithId" + ], + "type": "object" + }, "SyncRequestType": { "enum": [ - "UsersV1" + "UsersV1", + "PartnersV1" ], "type": "string" }, diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 9cdb5f991f..bdb9c08cd8 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,18 +1,18 @@ { "name": "@immich/sdk", - "version": "1.126.1", + "version": "1.129.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.126.1", + "version": "1.129.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "typescript": "^5.3.3" } }, @@ -23,9 +23,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", - "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "version": "22.13.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", + "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index be06fbdc4d..dfc4e63b0a 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.126.1", + "version": "1.129.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "typescript": "^5.3.3" }, "repository": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index bb97dbaf78..fb70a42e84 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.126.1 + * 1.129.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -3453,7 +3453,8 @@ export enum AlbumUserRole { } export enum SourceType { MachineLearning = "machine-learning", - Exif = "exif" + Exif = "exif", + Manual = "manual" } export enum AssetTypeEnum { Image = "IMAGE", @@ -3644,10 +3645,13 @@ export enum Error2 { } export enum SyncEntityType { UserV1 = "UserV1", - UserDeleteV1 = "UserDeleteV1" + UserDeleteV1 = "UserDeleteV1", + PartnerV1 = "PartnerV1", + PartnerDeleteV1 = "PartnerDeleteV1" } export enum SyncRequestType { - UsersV1 = "UsersV1" + UsersV1 = "UsersV1", + PartnersV1 = "PartnersV1" } export enum TranscodeHWAccel { Nvenc = "nvenc", diff --git a/renovate.json b/renovate.json index 2634eaef4d..c65ad02754 100644 --- a/renovate.json +++ b/renovate.json @@ -4,6 +4,7 @@ "config:recommended", "docker:pinDigests" ], + "recreateWhen": "never", "minimumReleaseAge": "5 days", "packageRules": [ { diff --git a/server/Dockerfile b/server/Dockerfile index 532c39c42e..f46ea8d0e3 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20250218@sha256:04df131dafca34538685453e4a00387ffe14288edff43cc68cf44feb76c8f4c0 AS dev +FROM ghcr.io/immich-app/base-server-dev:20250304@sha256:bc8d0c8d5c6d00625d01c84785435383651a9d57cb6cd1b1430cf0bcb58e4a80 AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl # web build -FROM node:22.13.1-alpine3.20@sha256:c52e20859a92b3eccbd3a36c5e1a90adc20617d8d421d65e8a622e87b5dac963 AS web +FROM node:22.14.0-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS web WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/server/package-lock.json b/server/package-lock.json index ce39195f22..f26bd163a6 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.126.1", + "version": "1.129.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich", - "version": "1.126.1", + "version": "1.129.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^11.0.1", @@ -66,7 +66,7 @@ "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", - "ua-parser-js": "^1.0.35", + "ua-parser-js": "^2.0.0", "validator": "^13.12.0" }, "devDependencies": { @@ -88,7 +88,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/pngjs": "^6.0.5", @@ -104,7 +104,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^56.0.1", - "globals": "^15.9.0", + "globals": "^16.0.0", "kysely-codegen": "^0.17.0", "mock-fs": "^5.2.0", "node-addon-api": "^8.3.0", @@ -150,13 +150,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.1.3.tgz", - "integrity": "sha512-DfN45eJQtfXXeQwjb7vDqSJ+8e6BW3rXUB2i6IC2CbOYrLWhMBgfv3/uTm++IbCFW2zX3Yk3yqq3d4yua2no7w==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.1.8.tgz", + "integrity": "sha512-2JGUMD3zjfY8G4RYpypm2/1YEO+O4DtFycUvptIpsBYyULgnEbJ3tlp2oRiXI2vp9tC8IyWqa/swlA8DTI6ZYQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.3", + "@angular-devkit/core": "19.1.8", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", "ora": "5.4.1", @@ -169,14 +169,14 @@ } }, "node_modules/@angular-devkit/schematics-cli": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.1.3.tgz", - "integrity": "sha512-levMPch+Mni/cEVd/b9RUzasxWqlafBVjgrofbaSlxgZmr4pRJ/tihzrNnygNUaXoBqhTtXU5aFxTGbJhS35eA==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-19.1.8.tgz", + "integrity": "sha512-sHblN9EuiJgKwJVYc+nhpU+GlVkAJHJc7lBR8YSoaugNGcCMkWn4f7rJnJDywL/CEOHBICnyWZKfTCMsMyg1Cw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.3", - "@angular-devkit/schematics": "19.1.3", + "@angular-devkit/core": "19.1.8", + "@angular-devkit/schematics": "19.1.8", "@inquirer/prompts": "7.2.1", "ansi-colors": "4.1.3", "symbol-observable": "4.0.0", @@ -192,9 +192,9 @@ } }, "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/core": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.3.tgz", - "integrity": "sha512-of/TKfJ/vL+/qvr4PbDTtqbFJGFHPfu6bEJrIZsLMYA+Mej8SyTx3kDm4LLnKQBtWVYDqkrxvcpOb4+NmHNLfA==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.8.tgz", + "integrity": "sha512-j1zHKvOsGwu5YwAZGuzi835R9vcW7PkfxmSRIJeVl+vawgk31K3zFb4UPH8AY/NPWYqXIAnwpka3HC1+JrWLWA==", "dev": true, "license": "MIT", "dependencies": { @@ -305,9 +305,9 @@ "license": "MIT" }, "node_modules/@angular-devkit/schematics-cli/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "optional": true, @@ -320,10 +320,20 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@angular-devkit/schematics-cli/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.3.tgz", - "integrity": "sha512-of/TKfJ/vL+/qvr4PbDTtqbFJGFHPfu6bEJrIZsLMYA+Mej8SyTx3kDm4LLnKQBtWVYDqkrxvcpOb4+NmHNLfA==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.8.tgz", + "integrity": "sha512-j1zHKvOsGwu5YwAZGuzi835R9vcW7PkfxmSRIJeVl+vawgk31K3zFb4UPH8AY/NPWYqXIAnwpka3HC1+JrWLWA==", "dev": true, "license": "MIT", "dependencies": { @@ -409,9 +419,9 @@ "license": "MIT" }, "node_modules/@angular-devkit/schematics/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "optional": true, @@ -424,6 +434,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -1133,13 +1153,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -1148,9 +1168,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1161,9 +1181,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1198,9 +1218,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", "dev": true, "license": "MIT", "engines": { @@ -1208,9 +1228,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1218,13 +1238,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.12.0", "levn": "^0.4.1" }, "engines": { @@ -1353,9 +1373,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1728,15 +1748,15 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.6.tgz", - "integrity": "sha512-PgP35JfmGjHU0LSXOyRew0zHuA9N6OJwOlos1fZ20b7j8ISeAdib3L+n0jIxBtX958UeEpte6xhG/gxJ5iUqMw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.2.tgz", + "integrity": "sha512-PL9ixC5YsPXzXhAZFUPmkXGxfgjkdfZdPEPPmt4kFwQ4LBMDG9n/nHXYRGGZSKZJs+d1sGKWgS2GiPzVRKUdtQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.7", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.4", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -1745,44 +1765,61 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/confirm": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.3.tgz", - "integrity": "sha512-fuF9laMmHoOgWapF9h9hv6opA5WvmGFHsTYGCmuFxcghIhEhb3dN0CdQR4BUMqa2H506NCj8cGX4jwMsE4t6dA==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.6.tgz", + "integrity": "sha512-6ZXYK3M1XmaVBZX6FCfChgtponnL0R6I7k8Nu+kaoNkT828FVZTcca1MqmWQipaW2oNREQl5AaPCUOOCVNdRMw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2" + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/core": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.4.tgz", - "integrity": "sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==", + "version": "10.1.7", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.7.tgz", + "integrity": "sha512-AA9CQhlrt6ZgiSy6qoAigiA1izOa751ugX6ioSjqgJ+/Gd+tEN/TORk5sUYNjXuHWfW0r1n/a6ak4u/NqHHrtA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.4", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", - "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/core/node_modules/cli-width": { @@ -1806,14 +1843,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.3.tgz", - "integrity": "sha512-S9KnIOJuTZpb9upeRSBBhoDZv7aSV3pG9TECrBj0f+ZsFwccz886hzKBrChGrXMJwd4NKY+pOA9Vy72uqnd6Eg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.7.tgz", + "integrity": "sha512-gktCSQtnSZHaBytkJKMKEuswSk2cDBuXX5rxGFv306mwHfBPjg5UAldw9zWGoEyvA9KpRDkeM4jfrx0rXn0GyA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4", "external-editor": "^3.1.0" }, "engines": { @@ -1821,17 +1858,22 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/expand": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.6.tgz", - "integrity": "sha512-TRTfi1mv1GeIZGyi9PQmvAaH65ZlG4/FACq6wSzs7Vvf1z5dnNWsAAXBjWMHt76l+1hUY8teIqJFrWBk5N6gsg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.9.tgz", + "integrity": "sha512-Xxt6nhomWTAmuSX61kVgglLjMEFGa+7+F6UUtdEUeg7fg4r9vaFttUUKrtkViYYrQBA5Ia1tkOJj2koP9BuLig==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -1839,12 +1881,17 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/figures": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.9.tgz", - "integrity": "sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.10.tgz", + "integrity": "sha512-Ey6176gZmeqZuY/W/nZiUyvmb1/qInjcpiZjXWi6nON+nxJpD1bxtSoBxNliGISae32n6OwbY+TSXPZ1CfS4bw==", "dev": true, "license": "MIT", "engines": { @@ -1852,48 +1899,58 @@ } }, "node_modules/@inquirer/input": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.3.tgz", - "integrity": "sha512-zeo++6f7hxaEe7OjtMzdGZPHiawsfmCZxWB9X1NpmYgbeoyerIbWemvlBxxl+sQIlHC0WuSAG19ibMq3gbhaqQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.6.tgz", + "integrity": "sha512-1f5AIsZuVjPT4ecA8AwaxDFNHny/tSershP/cTvTDxLdiIGTeILNcKozB0LaYt6mojJLUbOYhpIxicaYf7UKIQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2" + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/number": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.6.tgz", - "integrity": "sha512-xO07lftUHk1rs1gR0KbqB+LJPhkUNkyzV/KhH+937hdkMazmAYHLm1OIrNKpPelppeV1FgWrgFDjdUD8mM+XUg==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.9.tgz", + "integrity": "sha512-iN2xZvH3tyIYXLXBvlVh0npk1q/aVuKXZo5hj+K3W3D4ngAEq/DkLpofRzx6oebTUhBvOgryZ+rMV0yImKnG3w==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2" + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4" }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/password": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.6.tgz", - "integrity": "sha512-QLF0HmMpHZPPMp10WGXh6F+ZPvzWE7LX6rNoccdktv/Rov0B+0f+eyXkAcgqy5cH9V+WSpbLxu2lo3ysEVK91w==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.9.tgz", + "integrity": "sha512-xBEoOw1XKb0rIN208YU7wM7oJEHhIYkfG7LpTJAEW913GZeaoQerzf5U/LSHI45EVvjAdgNXmXgH51cUXKZcJQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4", "ansi-escapes": "^4.3.2" }, "engines": { @@ -1901,42 +1958,52 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/prompts": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.2.3.tgz", - "integrity": "sha512-hzfnm3uOoDySDXfDNOm9usOuYIaQvTgKp/13l1uJoe6UNY+Zpcn2RYt0jXz3yA+yemGHvDOxVzqWl3S5sQq53Q==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", + "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.0.6", - "@inquirer/confirm": "^5.1.3", - "@inquirer/editor": "^4.2.3", - "@inquirer/expand": "^4.0.6", - "@inquirer/input": "^4.1.3", - "@inquirer/number": "^3.0.6", - "@inquirer/password": "^4.0.6", - "@inquirer/rawlist": "^4.0.6", - "@inquirer/search": "^3.0.6", - "@inquirer/select": "^4.0.6" + "@inquirer/checkbox": "^4.1.2", + "@inquirer/confirm": "^5.1.6", + "@inquirer/editor": "^4.2.7", + "@inquirer/expand": "^4.0.9", + "@inquirer/input": "^4.1.6", + "@inquirer/number": "^3.0.9", + "@inquirer/password": "^4.0.9", + "@inquirer/rawlist": "^4.0.9", + "@inquirer/search": "^3.0.9", + "@inquirer/select": "^4.0.9" }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/rawlist": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.6.tgz", - "integrity": "sha512-QoE4s1SsIPx27FO4L1b1mUjVcoHm1pWE/oCmm4z/Hl+V1Aw5IXl8FYYzGmfXaBT0l/sWr49XmNSiq7kg3Kd/Lg==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.9.tgz", + "integrity": "sha512-+5t6ebehKqgoxV8fXwE49HkSF2Rc9ijNiVGEQZwvbMI61/Q5RcD+jWD6Gs1tKdz5lkI8GRBL31iO0HjGK1bv+A==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.7", + "@inquirer/type": "^3.0.4", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -1944,18 +2011,23 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/search": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.6.tgz", - "integrity": "sha512-eFZ2hiAq0bZcFPuFFBmZEtXU1EarHLigE+ENCtpO+37NHCl4+Yokq1P/d09kUblObaikwfo97w+0FtG/EXl5Ng==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.9.tgz", + "integrity": "sha512-DWmKztkYo9CvldGBaRMr0ETUHgR86zE6sPDVOHsqz4ISe9o1LuiWfgJk+2r75acFclA93J/lqzhT0dTjCzHuoA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.7", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.4", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -1963,18 +2035,23 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/select": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.6.tgz", - "integrity": "sha512-yANzIiNZ8fhMm4NORm+a74+KFYHmf7BZphSOBovIzYPVLquseTGEkU5l2UTnBOf5k0VLmTgPighNDLE9QtbViQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.9.tgz", + "integrity": "sha512-BpJyJe7Dkhv2kz7yG7bPSbJLQuu/rqyNlF1CfiiFeFwouegfH+zh13KDyt6+d9DwucKo7hqM3wKLLyJxZMO+Xg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.7", + "@inquirer/figures": "^1.0.10", + "@inquirer/type": "^3.0.4", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -1983,12 +2060,17 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/type": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz", - "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.4.tgz", + "integrity": "sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==", "dev": true, "license": "MIT", "engines": { @@ -1996,6 +2078,11 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@ioredis/commands": { @@ -2246,9 +2333,9 @@ } }, "node_modules/@microsoft/tsdoc": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", - "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { @@ -2358,18 +2445,18 @@ } }, "node_modules/@nestjs/cli": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.2.tgz", - "integrity": "sha512-y1dKk+Q94vnWhJe8eoz1Qs5WIYHSgO0xZttsFnDbYW1A6CBUVanc4RocbiyhwC/GjWPO4D5JmTXjW5mRH6wprA==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-11.0.5.tgz", + "integrity": "sha512-ab/d8Ple+dMSQ4pC7RSNjhntpT8gFQQE8y/F/ilaplp7zPGpuxbayRtYbsA/wc1UkJHORDckrqFc8Jh8mrTq2A==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.3", - "@angular-devkit/schematics": "19.1.3", - "@angular-devkit/schematics-cli": "19.1.3", - "@inquirer/prompts": "7.2.3", - "@nestjs/schematics": "11.0.0", - "ansis": "3.9.0", + "@angular-devkit/core": "19.1.8", + "@angular-devkit/schematics": "19.1.8", + "@angular-devkit/schematics-cli": "19.1.8", + "@inquirer/prompts": "7.3.2", + "@nestjs/schematics": "^11.0.1", + "ansis": "3.16.0", "chokidar": "4.0.3", "cli-table3": "0.6.5", "commander": "4.1.1", @@ -2381,7 +2468,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.2.0", "typescript": "5.7.3", - "webpack": "5.97.1", + "webpack": "5.98.0", "webpack-node-externals": "3.0.0" }, "bin": { @@ -2404,9 +2491,9 @@ } }, "node_modules/@nestjs/cli/node_modules/@angular-devkit/core": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.3.tgz", - "integrity": "sha512-of/TKfJ/vL+/qvr4PbDTtqbFJGFHPfu6bEJrIZsLMYA+Mej8SyTx3kDm4LLnKQBtWVYDqkrxvcpOb4+NmHNLfA==", + "version": "19.1.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.8.tgz", + "integrity": "sha512-j1zHKvOsGwu5YwAZGuzi835R9vcW7PkfxmSRIJeVl+vawgk31K3zFb4UPH8AY/NPWYqXIAnwpka3HC1+JrWLWA==", "dev": true, "license": "MIT", "dependencies": { @@ -2517,9 +2604,9 @@ } }, "node_modules/@nestjs/cli/node_modules/jackspeak": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", - "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", + "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -2583,9 +2670,9 @@ } }, "node_modules/@nestjs/cli/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "engines": { @@ -2596,10 +2683,20 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nestjs/cli/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@nestjs/common": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.9.tgz", - "integrity": "sha512-+SKMYQE7O55tJGQVibnAlR9sXRBCF/8gM1cILdLT7cLj7+51NYD7eAHuAcGYiQIyXsTehZoO7C+B7S3ibNi3aw==", + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.0.10.tgz", + "integrity": "sha512-pzGXp14KF2Q4CDZGQgPK4l8zEg7i6cNkb+10yc8ZA5K41cLe3ZbWW1YxtY2e/glHauOJwTLSVjH4tiRVtOTizg==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -2626,9 +2723,9 @@ } }, "node_modules/@nestjs/core": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.0.9.tgz", - "integrity": "sha512-w32ZF1acSnidiRERGRC/Ki7eSrnTdP7twRnjy15Qnmij3oYRCdgX5aZvbUNv17A7bn9S22p+wMNcpHxqwUiOsQ==", + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.0.10.tgz", + "integrity": "sha512-f0qB8ztNWZeAD4E4fUdHConmNYCa/A78U7WJu5mX9OLYfOAs3ESYCDfsH9MRUvkA4Ft4Y1uMmyJo5L4fg4+beg==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -2667,9 +2764,9 @@ } }, "node_modules/@nestjs/event-emitter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-3.0.0.tgz", - "integrity": "sha512-WbvzQQ9BGnj27onh2qSLND2+4iA6Pfp4K+HLlqunB0Uz0614O8lGMtcveSss2IOxsox8EhSI54WAvuAsDrX1hA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-3.0.1.tgz", + "integrity": "sha512-0Ln/x+7xkU6AJFOcQI9tIhUMXVF7D5itiaQGOyJbXtlAfAIt8gzDdJm+Im7cFzKoWkiW5nCXCPh6GSvdQd/3Dw==", "license": "MIT", "dependencies": { "eventemitter2": "6.4.9" @@ -2700,9 +2797,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.0.9.tgz", - "integrity": "sha512-AX3UbVNkK+yB0lxfwjy5krxLZy/bWD2V+3WIbc0btRQVcxGj4LWL0yenbAo0jiAFwW3rKt7SbmMHUuKK6OH5qA==", + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.0.10.tgz", + "integrity": "sha512-UVSf0yaWFBC2Zn2FOWABXxCnyG8XNIXrNnvTFpbUFqaJu1YDdwJ7wQBBqxq9CtJT7ILqSmfhOU7HS0d/0EAxpw==", "license": "MIT", "dependencies": { "cors": "2.8.5", @@ -2721,9 +2818,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.0.9.tgz", - "integrity": "sha512-H+wTOollSZ1GSAp2+UIl4x+rYutkrH0poYeeo/+Dr8AwobghZuSGxoQ3FpqIurI/NuwBoBdFJV7cm7v9mlKkuw==", + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-11.0.10.tgz", + "integrity": "sha512-39lAjq0+kZRiMuscDcugoG+onPDciM4jhuf8ZDjVcuSwtib1OGwrFtErSzp/KJsmHPSStgapbNev7eFi32uWQA==", "license": "MIT", "dependencies": { "socket.io": "4.8.1", @@ -2753,14 +2850,14 @@ } }, "node_modules/@nestjs/schematics": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.0.tgz", - "integrity": "sha512-wts8lG0GfNWw3Wk9aaG5I/wcMIAdm7HjjeThQfUZhJxeIFT82Z3F5+0cYdHH4ii2pYQGiCSrR1VcuMwPiHoecg==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-11.0.1.tgz", + "integrity": "sha512-PHPAUk4sXkfCxiMacD1JFC+vEyzXjZJRCu1KT2MmG2hrTiMDMk5KtMprro148JUefNuWbVyN0uLTJVSmWVzhoA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.0.1", - "@angular-devkit/schematics": "19.0.1", + "@angular-devkit/core": "19.1.7", + "@angular-devkit/schematics": "19.1.7", "comment-json": "4.2.5", "jsonc-parser": "3.3.1", "pluralize": "8.0.0" @@ -2770,9 +2867,9 @@ } }, "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.0.1.tgz", - "integrity": "sha512-oXIAV3hXqUW3Pmm95pvEmb+24n1cKQG62FzhQSjOIrMeHiCbGLNuc8zHosIi2oMrcCJJxR6KzWjThvbuzDwWlw==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.7.tgz", + "integrity": "sha512-q0I6L9KTqyQ7D5M8H+fWLT+yjapvMNb7SRdfU6GzmexO66Dpo83q4HDzuDKIPDF29Yl0ELs9ICJqe9yUXh6yDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2798,15 +2895,15 @@ } }, "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { - "version": "19.0.1", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.0.1.tgz", - "integrity": "sha512-N9dV8WpNRULykNj8fSxQrta85gPKxb315J3xugLS2uwiFWhz7wo5EY1YeYhoVKoVcNB2ng9imJgC5aO52AHZwg==", + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.1.7.tgz", + "integrity": "sha512-AP6FvhMybCYs3gs+vzEAzSU1K//AFT3SVTRFv+C3WMO5dLeAHeGzM8I2dxD5EHQQtqIE/8apP6CxGrnpA5YlFg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.0.1", + "@angular-devkit/core": "19.1.7", "jsonc-parser": "3.3.1", - "magic-string": "0.30.12", + "magic-string": "0.30.17", "ora": "5.4.1", "rxjs": "7.8.1" }, @@ -2876,20 +2973,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@nestjs/schematics/node_modules/magic-string": { - "version": "0.30.12", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", - "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, "node_modules/@nestjs/schematics/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", "optional": true, @@ -2902,18 +2989,28 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nestjs/schematics/node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/@nestjs/swagger": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.0.3.tgz", - "integrity": "sha512-oyrhrAzVJz1wYefIYDb6Y0f1VYb8BtYxEI7Ex0ApoUsfGZThyhW9elYANcfBXVaTmICrU8lCESF2ygF6s0ThIw==", + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.0.5.tgz", + "integrity": "sha512-3z4rl7FgbLPBvJwR45nBDju4QFH7vufs9Ums8sCoc6T1O1dqpNpxz0sKfXiP5QA6AUljks1jARfOHeHeJ4zWBA==", "license": "MIT", "dependencies": { - "@microsoft/tsdoc": "0.15.0", + "@microsoft/tsdoc": "0.15.1", "@nestjs/mapped-types": "2.1.0", "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "8.2.0", - "swagger-ui-dist": "5.18.2" + "swagger-ui-dist": "5.19.0" }, "peerDependencies": { "@fastify/static": "^8.0.0", @@ -2936,9 +3033,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.9.tgz", - "integrity": "sha512-49UV5tC4N0pLf6waVjBVr/CyTkQPGZG/wLmbmu1INd/brprKOJ2lT3nl+6qfWJIqGbxGqZHUQr5FcDk+T/w9lQ==", + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.0.10.tgz", + "integrity": "sha512-uZcdnvmHXWnvozYOAwZi1elpRRfqIfYqHglCavjhjcj3cH1MVZkwoTqntW3XOPQlT4lf96InjP1exGaW4B9wUg==", "dev": true, "license": "MIT", "dependencies": { @@ -2977,9 +3074,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "11.0.9", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.0.9.tgz", - "integrity": "sha512-+eWh4MPTYBCoCQgcTY6g8TNPg0FkKzoqWRuugoA95Zz5edldoEK/P4BkqvzBiwrKqoHKwpsM/cct2edFA1TzQg==", + "version": "11.0.10", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-11.0.10.tgz", + "integrity": "sha512-GPIEfqJyAkTHrHGK9w2OU8LJaZAZKW8WpWcTplThLxMelRq7mBkYOaGvc6dpr7fE1wWzWkwY0ZjQEnwnVmmxSg==", "license": "MIT", "dependencies": { "iterare": "1.2.1", @@ -3206,58 +3303,58 @@ } }, "node_modules/@opentelemetry/auto-instrumentations-node": { - "version": "0.56.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.56.0.tgz", - "integrity": "sha512-d1X3DQY0+VmhNUir/3U3JO6Uh0FOSm8G91zsPzVVKc6NGDwmHP6Dn7PMVH70O6FZ0yErzlHqRx8vkNiAsTWt5A==", + "version": "0.56.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.56.1.tgz", + "integrity": "sha512-4cK0+unfkXRRbQQg2r9K3ki8JlE0j9Iw8+4DZEkChShAnmviiE+/JMgHGvK+VVcLrSlgV6BBHv4+ZTLukQwhkA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/instrumentation-amqplib": "^0.46.0", - "@opentelemetry/instrumentation-aws-lambda": "^0.50.2", - "@opentelemetry/instrumentation-aws-sdk": "^0.49.0", - "@opentelemetry/instrumentation-bunyan": "^0.45.0", - "@opentelemetry/instrumentation-cassandra-driver": "^0.45.0", - "@opentelemetry/instrumentation-connect": "^0.43.0", - "@opentelemetry/instrumentation-cucumber": "^0.14.0", - "@opentelemetry/instrumentation-dataloader": "^0.16.0", - "@opentelemetry/instrumentation-dns": "^0.43.0", - "@opentelemetry/instrumentation-express": "^0.47.0", - "@opentelemetry/instrumentation-fastify": "^0.44.1", - "@opentelemetry/instrumentation-fs": "^0.19.0", - "@opentelemetry/instrumentation-generic-pool": "^0.43.0", - "@opentelemetry/instrumentation-graphql": "^0.47.0", - "@opentelemetry/instrumentation-grpc": "^0.57.0", - "@opentelemetry/instrumentation-hapi": "^0.45.1", - "@opentelemetry/instrumentation-http": "^0.57.0", - "@opentelemetry/instrumentation-ioredis": "^0.47.0", - "@opentelemetry/instrumentation-kafkajs": "^0.7.0", - "@opentelemetry/instrumentation-knex": "^0.44.0", - "@opentelemetry/instrumentation-koa": "^0.47.0", - "@opentelemetry/instrumentation-lru-memoizer": "^0.44.0", - "@opentelemetry/instrumentation-memcached": "^0.43.0", - "@opentelemetry/instrumentation-mongodb": "^0.51.0", - "@opentelemetry/instrumentation-mongoose": "^0.46.0", - "@opentelemetry/instrumentation-mysql": "^0.45.0", - "@opentelemetry/instrumentation-mysql2": "^0.45.1", - "@opentelemetry/instrumentation-nestjs-core": "^0.44.0", - "@opentelemetry/instrumentation-net": "^0.43.0", - "@opentelemetry/instrumentation-pg": "^0.51.0", - "@opentelemetry/instrumentation-pino": "^0.46.0", - "@opentelemetry/instrumentation-redis": "^0.46.0", - "@opentelemetry/instrumentation-redis-4": "^0.46.0", - "@opentelemetry/instrumentation-restify": "^0.45.0", - "@opentelemetry/instrumentation-router": "^0.44.0", - "@opentelemetry/instrumentation-socket.io": "^0.46.0", - "@opentelemetry/instrumentation-tedious": "^0.18.0", - "@opentelemetry/instrumentation-undici": "^0.10.0", - "@opentelemetry/instrumentation-winston": "^0.44.0", - "@opentelemetry/resource-detector-alibaba-cloud": "^0.30.0", - "@opentelemetry/resource-detector-aws": "^1.11.0", - "@opentelemetry/resource-detector-azure": "^0.6.0", - "@opentelemetry/resource-detector-container": "^0.6.0", - "@opentelemetry/resource-detector-gcp": "^0.33.0", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/instrumentation-amqplib": "^0.46.1", + "@opentelemetry/instrumentation-aws-lambda": "^0.50.3", + "@opentelemetry/instrumentation-aws-sdk": "^0.49.1", + "@opentelemetry/instrumentation-bunyan": "^0.45.1", + "@opentelemetry/instrumentation-cassandra-driver": "^0.45.1", + "@opentelemetry/instrumentation-connect": "^0.43.1", + "@opentelemetry/instrumentation-cucumber": "^0.14.1", + "@opentelemetry/instrumentation-dataloader": "^0.16.1", + "@opentelemetry/instrumentation-dns": "^0.43.1", + "@opentelemetry/instrumentation-express": "^0.47.1", + "@opentelemetry/instrumentation-fastify": "^0.44.2", + "@opentelemetry/instrumentation-fs": "^0.19.1", + "@opentelemetry/instrumentation-generic-pool": "^0.43.1", + "@opentelemetry/instrumentation-graphql": "^0.47.1", + "@opentelemetry/instrumentation-grpc": "^0.57.1", + "@opentelemetry/instrumentation-hapi": "^0.45.2", + "@opentelemetry/instrumentation-http": "^0.57.1", + "@opentelemetry/instrumentation-ioredis": "^0.47.1", + "@opentelemetry/instrumentation-kafkajs": "^0.7.1", + "@opentelemetry/instrumentation-knex": "^0.44.1", + "@opentelemetry/instrumentation-koa": "^0.47.1", + "@opentelemetry/instrumentation-lru-memoizer": "^0.44.1", + "@opentelemetry/instrumentation-memcached": "^0.43.1", + "@opentelemetry/instrumentation-mongodb": "^0.52.0", + "@opentelemetry/instrumentation-mongoose": "^0.46.1", + "@opentelemetry/instrumentation-mysql": "^0.45.1", + "@opentelemetry/instrumentation-mysql2": "^0.45.2", + "@opentelemetry/instrumentation-nestjs-core": "^0.44.1", + "@opentelemetry/instrumentation-net": "^0.43.1", + "@opentelemetry/instrumentation-pg": "^0.51.1", + "@opentelemetry/instrumentation-pino": "^0.46.1", + "@opentelemetry/instrumentation-redis": "^0.46.1", + "@opentelemetry/instrumentation-redis-4": "^0.46.1", + "@opentelemetry/instrumentation-restify": "^0.45.1", + "@opentelemetry/instrumentation-router": "^0.44.1", + "@opentelemetry/instrumentation-socket.io": "^0.46.1", + "@opentelemetry/instrumentation-tedious": "^0.18.1", + "@opentelemetry/instrumentation-undici": "^0.10.1", + "@opentelemetry/instrumentation-winston": "^0.44.1", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.30.1", + "@opentelemetry/resource-detector-aws": "^1.12.0", + "@opentelemetry/resource-detector-azure": "^0.6.1", + "@opentelemetry/resource-detector-container": "^0.6.1", + "@opentelemetry/resource-detector-gcp": "^0.33.1", "@opentelemetry/resources": "^1.24.0", - "@opentelemetry/sdk-node": "^0.57.0" + "@opentelemetry/sdk-node": "^0.57.1" }, "engines": { "node": ">=14" @@ -3569,13 +3666,13 @@ } }, "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.0.tgz", - "integrity": "sha512-04VHHV1KIN/c1wLWwzmLI02d/welgscBJ4BuDqrHaxd+ZIdlVXK9UYQsYf3JwSeF52z/4YoSzr8bfdVBSWoMAg==", + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz", + "integrity": "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3586,14 +3683,14 @@ } }, "node_modules/@opentelemetry/instrumentation-aws-lambda": { - "version": "0.50.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.50.2.tgz", - "integrity": "sha512-jz1a7t2q0SsiztEMyZjFLEFC4pOQ+1C588gWzl878k9Qr6TI1Wu3sa7/dikxJmeRIETcOTUilaa2Otxh6HUVlA==", + "version": "0.50.3", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.50.3.tgz", + "integrity": "sha512-kotm/mRvSWUauudxcylc5YCDei+G/r+jnOH6q5S99aPLQ/Ms8D2yonMIxEJUILIPlthEmwLYxkw3ualWzMjm/A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/aws-lambda": "8.10.143" + "@types/aws-lambda": "8.10.147" }, "engines": { "node": ">=14" @@ -3603,14 +3700,14 @@ } }, "node_modules/@opentelemetry/instrumentation-aws-sdk": { - "version": "0.49.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.49.0.tgz", - "integrity": "sha512-m3yC3ni4Yo8tggbZgygS/ccAP9e/EYqsMwzooHiIymbnyZwDAB7kMZ3OrjcLVPCFx9gjNMDKW4MdwOPC0vTEeQ==", + "version": "0.49.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.49.1.tgz", + "integrity": "sha512-Vbj4BYeV/1K4Pbbfk+gQ8gwYL0w+tBeUwG88cOxnF7CLPO1XnskGV8Q3Gzut2Ah/6Dg17dBtlzEqL3UiFP2Z6A==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/propagation-utils": "^0.30.15", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/propagation-utils": "^0.30.16", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3621,14 +3718,14 @@ } }, "node_modules/@opentelemetry/instrumentation-bunyan": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.45.0.tgz", - "integrity": "sha512-K3ZleoOxKUzGjt0TfAT1jfSNcgyt7+toqjhWymPf2tsGUETXxaxGDzAoNepWcfIkgPauJLPpRLLKcP6LjYLILw==", + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.45.1.tgz", + "integrity": "sha512-T9POV9ccS41UjpsjLrJ4i0m8LfplBiN3dMeH9XZ2btiDrjoaWtDrst6tNb1avetBjkeshOuBp1EWKP22EVSr0g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.57.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@types/bunyan": "1.8.9" + "@opentelemetry/api-logs": "^0.57.1", + "@opentelemetry/instrumentation": "^0.57.1", + "@types/bunyan": "1.8.11" }, "engines": { "node": ">=14" @@ -3638,12 +3735,12 @@ } }, "node_modules/@opentelemetry/instrumentation-cassandra-driver": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.45.0.tgz", - "integrity": "sha512-IKoA0lLfF7EyIL85MfqzvfAa/Oz9zHNFXwzSiQ6Iqej89BMyOm3eYaAsyUDAvgiLG12M189temMMyRuR07YsZg==", + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.45.1.tgz", + "integrity": "sha512-RqnP0rK2hcKK1AKcmYvedLiL6G5TvFGiSUt2vI9wN0cCBdTt9Y9+wxxY19KoGxq7e9T/aHow6P5SUhCVI1sHvQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3654,15 +3751,15 @@ } }, "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.0.tgz", - "integrity": "sha512-Q57JGpH6T4dkYHo9tKXONgLtxzsh1ZEW5M9A/OwKrZFyEpLqWgjhcZ3hIuVvDlhb426iDF1f9FPToV/mi5rpeA==", + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.1.tgz", + "integrity": "sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/connect": "3.4.36" + "@types/connect": "3.4.38" }, "engines": { "node": ">=14" @@ -3672,12 +3769,12 @@ } }, "node_modules/@opentelemetry/instrumentation-cucumber": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.14.0.tgz", - "integrity": "sha512-i/GlurL1IM+CnbmItW8kx59YxAp0wu/YQkzQQRU/YGmUjym5g+/dOVjnk/K46lAU49Nn1XyFd7S3ZNf83PHL2Q==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.14.1.tgz", + "integrity": "sha512-ybO+tmH85pDO0ywTskmrMtZcccKyQr7Eb7wHy1keR2HFfx46SzZbjHo1AuGAX//Hook3gjM7+w211gJ2bwKe1Q==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3688,12 +3785,12 @@ } }, "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.0.tgz", - "integrity": "sha512-88+qCHZC02up8PwKHk0UQKLLqGGURzS3hFQBZC7PnGwReuoKjHXS1o29H58S+QkXJpkTr2GACbx8j6mUoGjNPA==", + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.1.tgz", + "integrity": "sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0" + "@opentelemetry/instrumentation": "^0.57.1" }, "engines": { "node": ">=14" @@ -3703,12 +3800,12 @@ } }, "node_modules/@opentelemetry/instrumentation-dns": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.43.0.tgz", - "integrity": "sha512-bGXTyBpjSYt6B7LEj0zMfWkoveGpYf5pVEgTZmDacsG49RdfdCH5PYt3C8MEMwYEFtu2dGdKdKa2LHfefIIDdg==", + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.43.1.tgz", + "integrity": "sha512-e/tMZYU1nc+k+J3259CQtqVZIPsPRSLNoAQbGEmSKrjLEY/KJSbpBZ17lu4dFVBzqoF1cZYIZxn9WPQxy4V9ng==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0" + "@opentelemetry/instrumentation": "^0.57.1" }, "engines": { "node": ">=14" @@ -3718,13 +3815,13 @@ } }, "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.0.tgz", - "integrity": "sha512-XFWVx6k0XlU8lu6cBlCa29ONtVt6ADEjmxtyAyeF2+rifk8uBJbk1La0yIVfI0DoKURGbaEDTNelaXG9l/lNNQ==", + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.1.tgz", + "integrity": "sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3735,13 +3832,13 @@ } }, "node_modules/@opentelemetry/instrumentation-fastify": { - "version": "0.44.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.44.1.tgz", - "integrity": "sha512-RoVeMGKcNttNfXMSl6W4fsYoCAYP1vi6ZAWIGhBY+o7R9Y0afA7f9JJL0j8LHbyb0P0QhSYk+6O56OwI2k4iRQ==", + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.44.2.tgz", + "integrity": "sha512-arSp97Y4D2NWogoXRb8CzFK3W2ooVdvqRRtQDljFt9uC3zI6OuShgey6CVFC0JxT1iGjkAr1r4PDz23mWrFULQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3752,13 +3849,13 @@ } }, "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.0.tgz", - "integrity": "sha512-JGwmHhBkRT2G/BYNV1aGI+bBjJu4fJUD/5/Jat0EWZa2ftrLV3YE8z84Fiij/wK32oMZ88eS8DI4ecLGZhpqsQ==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.1.tgz", + "integrity": "sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0" + "@opentelemetry/instrumentation": "^0.57.1" }, "engines": { "node": ">=14" @@ -3768,12 +3865,12 @@ } }, "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.0.tgz", - "integrity": "sha512-at8GceTtNxD1NfFKGAuwtqM41ot/TpcLh+YsGe4dhf7gvv1HW/ZWdq6nfRtS6UjIvZJOokViqLPJ3GVtZItAnQ==", + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.1.tgz", + "integrity": "sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0" + "@opentelemetry/instrumentation": "^0.57.1" }, "engines": { "node": ">=14" @@ -3783,12 +3880,12 @@ } }, "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.0.tgz", - "integrity": "sha512-Cc8SMf+nLqp0fi8oAnooNEfwZWFnzMiBHCGmDFYqmgjPylyLmi83b+NiTns/rKGwlErpW0AGPt0sMpkbNlzn8w==", + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.1.tgz", + "integrity": "sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0" + "@opentelemetry/instrumentation": "^0.57.1" }, "engines": { "node": ">=14" @@ -3814,13 +3911,13 @@ } }, "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.45.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.1.tgz", - "integrity": "sha512-VH6mU3YqAKTePPfUPwfq4/xr049774qWtfTuJqVHoVspCLiT3bW+fCQ1toZxt6cxRPYASoYaBsMA3CWo8B8rcw==", + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.2.tgz", + "integrity": "sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3850,12 +3947,12 @@ } }, "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.0.tgz", - "integrity": "sha512-4HqP9IBC8e7pW9p90P3q4ox0XlbLGme65YTrA3UTLvqvo4Z6b0puqZQP203YFu8m9rE/luLfaG7/xrwwqMUpJw==", + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.1.tgz", + "integrity": "sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/redis-common": "^0.36.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, @@ -3867,12 +3964,12 @@ } }, "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.0.tgz", - "integrity": "sha512-LB+3xiNzc034zHfCtgs4ITWhq6Xvdo8bsq7amR058jZlf2aXXDrN9SV4si4z2ya9QX4tz6r4eZJwDkXOp14/AQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.1.tgz", + "integrity": "sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3883,12 +3980,12 @@ } }, "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.0.tgz", - "integrity": "sha512-SlT0+bLA0Lg3VthGje+bSZatlGHw/vwgQywx0R/5u9QC59FddTQSPJeWNw29M6f8ScORMeUOOTwihlQAn4GkJQ==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.1.tgz", + "integrity": "sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3899,13 +3996,13 @@ } }, "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.47.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.0.tgz", - "integrity": "sha512-HFdvqf2+w8sWOuwtEXayGzdZ2vWpCKEQv5F7+2DSA74Te/Cv4rvb2E5So5/lh+ok4/RAIPuvCbCb/SHQFzMmbw==", + "version": "0.47.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.1.tgz", + "integrity": "sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3916,12 +4013,12 @@ } }, "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.0.tgz", - "integrity": "sha512-Tn7emHAlvYDFik3vGU0mdwvWJDwtITtkJ+5eT2cUquct6nIs+H8M47sqMJkCpyPe5QIBJoTOHxmc6mj9lz6zDw==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.1.tgz", + "integrity": "sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0" + "@opentelemetry/instrumentation": "^0.57.1" }, "engines": { "node": ">=14" @@ -3931,12 +4028,12 @@ } }, "node_modules/@opentelemetry/instrumentation-memcached": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.43.0.tgz", - "integrity": "sha512-qjldZMBpfxKwI4ODytX6raF1WE+Qov0wTW4+tkofjas1b8e0WmVs+Pw4/YlmjJNOKRLD1usYkP7QlmPLvyzZSA==", + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.43.1.tgz", + "integrity": "sha512-rK5YWC22gmsLp2aEbaPk5F+9r6BFFZuc9GTnW/ErrWpz2XNHUgeFInoPDg4t+Trs8OttIfn8XwkfFkSKqhxanw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/memcached": "^2.2.6" }, @@ -3948,12 +4045,12 @@ } }, "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.51.0.tgz", - "integrity": "sha512-cMKASxCX4aFxesoj3WK8uoQ0YUrRvnfxaO72QWI2xLu5ZtgX/QvdGBlU3Ehdond5eb74c2s1cqRQUIptBnKz1g==", + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.52.0.tgz", + "integrity": "sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3964,13 +4061,13 @@ } }, "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.0.tgz", - "integrity": "sha512-mtVv6UeaaSaWTeZtLo4cx4P5/ING2obSqfWGItIFSunQBrYROfhuVe7wdIrFUs2RH1tn2YYpAJyMaRe/bnTTIQ==", + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.1.tgz", + "integrity": "sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -3981,12 +4078,12 @@ } }, "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.0.tgz", - "integrity": "sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg==", + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.1.tgz", + "integrity": "sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/mysql": "2.15.26" }, @@ -3998,12 +4095,12 @@ } }, "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.45.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.1.tgz", - "integrity": "sha512-9R/vxEc02vlSqyQSmXRTvFMZVht8vgSJokKhiWA3z8Idu0mmdKFKeHiuW5yRGxM/WOi+7DWqQfYM7zw/cJc3sA==", + "version": "0.45.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.2.tgz", + "integrity": "sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.40.1" }, @@ -4015,12 +4112,12 @@ } }, "node_modules/@opentelemetry/instrumentation-nestjs-core": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.44.0.tgz", - "integrity": "sha512-t16pQ7A4WYu1yyQJZhRKIfUNvl5PAaF2pEteLvgJb/BWdd1oNuU1rOYt4S825kMy+0q4ngiX281Ss9qiwHfxFQ==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.44.1.tgz", + "integrity": "sha512-4TXaqJK27QXoMqrt4+hcQ6rKFd8B6V4JfrTJKnqBmWR1cbaqd/uwyl9yxhNH1JEkyo8GaBfdpBC4ZE4FuUhPmg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -4031,12 +4128,12 @@ } }, "node_modules/@opentelemetry/instrumentation-net": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.43.0.tgz", - "integrity": "sha512-jFzYpCGg1+s4uePNC86GcdzsYzDZpfVMDsHNZzw5MX6tMWyc2jtiXBFWed41HpWOtkIRU/SJd7KR0k1WjNZRuQ==", + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.43.1.tgz", + "integrity": "sha512-TaMqP6tVx9/SxlY81dHlSyP5bWJIKq+K7vKfk4naB/LX4LBePPY3++1s0edpzH+RfwN+tEGVW9zTb9ci0up/lQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -4047,13 +4144,13 @@ } }, "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.51.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.51.0.tgz", - "integrity": "sha512-/NStIcUWUofc11dL7tSgMk25NqvhtbHDCncgm+yc4iJF8Ste2Q/lwUitjfxqj4qWM280uFmBEtcmtMMjbjRU7Q==", + "version": "0.51.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.51.1.tgz", + "integrity": "sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.26.0", - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.40.1", "@types/pg": "8.6.1", @@ -4067,14 +4164,14 @@ } }, "node_modules/@opentelemetry/instrumentation-pino": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.46.0.tgz", - "integrity": "sha512-TFjW24fwc/5KafDZuXbdViGiTym/6U6tDnOEkM5K9LIKsySMWb8xNIVE7y/6B8zDwImncEssNN1t42NixQJqug==", + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.46.1.tgz", + "integrity": "sha512-HB8gD/9CNAKlTV+mdZehnFC4tLUtQ7e+729oGq88e4WipxzZxmMYuRwZ2vzOA9/APtq+MRkERJ9PcoDqSIjZ+g==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.57.0", + "@opentelemetry/api-logs": "^0.57.1", "@opentelemetry/core": "^1.25.0", - "@opentelemetry/instrumentation": "^0.57.0" + "@opentelemetry/instrumentation": "^0.57.1" }, "engines": { "node": ">=14" @@ -4084,12 +4181,12 @@ } }, "node_modules/@opentelemetry/instrumentation-redis": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.46.0.tgz", - "integrity": "sha512-dXgSf+h+v3Bl4/NYzcSHG0NtqbXz74ph9J1ZBwxTnaB79u+C+ntfqtNt9jklIEAEZ1jR0jRCsVbiZyOpoCpTOg==", + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.46.1.tgz", + "integrity": "sha512-AN7OvlGlXmlvsgbLHs6dS1bggp6Fcki+GxgYZdSrb/DB692TyfjR7sVILaCe0crnP66aJuXsg9cge3hptHs9UA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/redis-common": "^0.36.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, @@ -4101,12 +4198,12 @@ } }, "node_modules/@opentelemetry/instrumentation-redis-4": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.0.tgz", - "integrity": "sha512-aTUWbzbFMFeRODn3720TZO0tsh/49T8H3h8vVnVKJ+yE36AeW38Uj/8zykQ/9nO8Vrtjr5yKuX3uMiG/W8FKNw==", + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.1.tgz", + "integrity": "sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/redis-common": "^0.36.2", "@opentelemetry/semantic-conventions": "^1.27.0" }, @@ -4118,13 +4215,13 @@ } }, "node_modules/@opentelemetry/instrumentation-restify": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.45.0.tgz", - "integrity": "sha512-CJ5vq14Plh4W4382Jd/jpNEJStqwqbCzZH1Op4EZVPxXhYOwCafgyflOqjxXSzTvqzhaPDT+A079ix5ebQUlYw==", + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.45.1.tgz", + "integrity": "sha512-Zd6Go9iEa+0zcoA2vDka9r/plYKaT3BhD3ESIy4JNIzFWXeQBGbH3zZxQIsz0jbNTMEtonlymU7eTLeaGWiApA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -4135,12 +4232,12 @@ } }, "node_modules/@opentelemetry/instrumentation-router": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.44.0.tgz", - "integrity": "sha512-rmQZKYcof4M6vQjwtrlfybQo7BSD0mxkXdhfNHWxFjxOFGw9i7EuXSYLnThcVAqNnJ1EljzZiHzaJiq5Ehcb3A==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.44.1.tgz", + "integrity": "sha512-l4T/S7ByjpY5TCUPeDe1GPns02/5BpR0jroSMexyH3ZnXJt9PtYqx1IKAlOjaFEGEOQF2tGDsMi4PY5l+fSniQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -4151,12 +4248,12 @@ } }, "node_modules/@opentelemetry/instrumentation-socket.io": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.46.0.tgz", - "integrity": "sha512-BU3XGT63ziF0S9Ky0YevCuMhHUq6U+Wi1g/piJcB16nOqlfd1SW6EACl5LrUe+aNZk2qIXfuS7YV8R+H99+XQQ==", + "version": "0.46.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.46.1.tgz", + "integrity": "sha512-9AsCVUAHOqvfe2RM/2I0DsDnx2ihw1d5jIN4+Bly1YPFTJIbk4+bXjAkr9+X6PUfhiV5urQHZkiYYPU1Q4yzPA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { @@ -4167,12 +4264,12 @@ } }, "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.0.tgz", - "integrity": "sha512-9zhjDpUDOtD+coeADnYEJQ0IeLVCj7w/hqzIutdp5NqS1VqTAanaEfsEcSypyvYv5DX3YOsTUoF+nr2wDXPETA==", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.1.tgz", + "integrity": "sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.57.0", + "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/tedious": "^4.0.14" }, @@ -4184,13 +4281,13 @@ } }, "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.0.tgz", - "integrity": "sha512-vm+V255NGw9gaSsPD6CP0oGo8L55BffBc8KnxqsMuc6XiAD1L8SFNzsW0RHhxJFqy9CJaJh+YiJ5EHXuZ5rZBw==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.1.tgz", + "integrity": "sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.0" + "@opentelemetry/instrumentation": "^0.57.1" }, "engines": { "node": ">=14" @@ -4200,13 +4297,13 @@ } }, "node_modules/@opentelemetry/instrumentation-winston": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.44.0.tgz", - "integrity": "sha512-2uIrdmDIU9qJuHHKXTI3Gef+tNQmKtcwXDA6S0tm+KpKgkMwZB6AC0rNmGNQsxbGJSORj0NJvy5TVvk6jjsaqg==", + "version": "0.44.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.44.1.tgz", + "integrity": "sha512-iexblTsT3fP0hHUz/M1mWr+Ylg3bsYN2En/jvKXZtboW3Qkvt17HrQJYTF9leVIkXAfN97QxAcTE99YGbQW7vQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.57.0", - "@opentelemetry/instrumentation": "^0.57.0" + "@opentelemetry/api-logs": "^0.57.1", + "@opentelemetry/instrumentation": "^0.57.1" }, "engines": { "node": ">=14" @@ -4283,9 +4380,9 @@ } }, "node_modules/@opentelemetry/propagation-utils": { - "version": "0.30.15", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.15.tgz", - "integrity": "sha512-nQ30K+eXTkd9Kt8yep9FPrqogS712GvdkV6R1T+xZMSZnFrRCyZuWxMtP3+s3hrK2HWw3ti4lsIfBzsHWYiyrA==", + "version": "0.30.16", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.16.tgz", + "integrity": "sha512-ZVQ3Z/PQ+2GQlrBfbMMMT0U7MzvYZLCPP800+ooyaBqm4hMvuQHfP028gB9/db0mwkmyEAMad9houukUVxhwcw==", "license": "Apache-2.0", "engines": { "node": ">=14" @@ -4334,9 +4431,9 @@ } }, "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.30.0.tgz", - "integrity": "sha512-CniMuVcJENb7e6ljXC8BuE8xyHKV6kjHjFzAjbeK7BIq2JSPOqfvC+jjhUYnnSGFnDyoZxJCIbt6XIdwPWRPhg==", + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.30.1.tgz", + "integrity": "sha512-9l0FVP3F4+Z6ax27vMzkmhZdNtxAbDqEfy7rduzya3xFLaRiJSvOpw6cru6Edl5LwO+WvgNui+VzHa9ViE8wCg==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.26.0", @@ -4351,9 +4448,9 @@ } }, "node_modules/@opentelemetry/resource-detector-aws": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-1.11.0.tgz", - "integrity": "sha512-j7qQ75enAJrlSPkPowasScuukZ2ffFG659rhxOpUM4dBe/O8Jpq+dy4pIdFtjWKkM9i7LgisdUt/GW7wGIWoEQ==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-aws/-/resource-detector-aws-1.12.0.tgz", + "integrity": "sha512-Cvi7ckOqiiuWlHBdA1IjS0ufr3sltex2Uws2RK6loVp4gzIJyOijsddAI6IZ5kiO8h/LgCWe8gxPmwkTKImd+Q==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.0.0", @@ -4368,9 +4465,9 @@ } }, "node_modules/@opentelemetry/resource-detector-azure": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.6.0.tgz", - "integrity": "sha512-cQbR/x9IhCYk47GWt4uC1G5yQN8JJ02Ec8uT38fj7uIXRbAARulwGr7Ax0dUo0eAtXEKQ+fXdzkLR1Am8cw4mg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.6.1.tgz", + "integrity": "sha512-Djr31QCExVfWViaf9cGJnH+bUInD72p0GEfgDGgjCAztyvyji6WJvKjs6qmkpPN+Ig6KLk0ho2VgzT5Kdl4L2Q==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.25.1", @@ -4385,9 +4482,9 @@ } }, "node_modules/@opentelemetry/resource-detector-container": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.6.0.tgz", - "integrity": "sha512-HxOzOsGlIjAbnTjwRBWQOsjrQIZ4NnQaaBc6noO8fW0v9ahyRxzwDFVr/3X1kSYLnpr2RGeWmMGDX6VcHECsLA==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.6.1.tgz", + "integrity": "sha512-o4sLzx149DQXDmVa8pgjBDEEKOj9SuQnkSLbjUVOpQNnn10v0WNR6wLwh30mFsK26xOJ6SpqZBGKZiT7i5MjlA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.26.0", @@ -4402,9 +4499,9 @@ } }, "node_modules/@opentelemetry/resource-detector-gcp": { - "version": "0.33.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.33.0.tgz", - "integrity": "sha512-y368hq2UM6j42Py7xlR4rTfl+wC4CdGNGT38nqW+6BwGTQso0NC/GeifcwqorEKs/JWU9azA6XNDyUBNEjFpGA==", + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.33.1.tgz", + "integrity": "sha512-/aZJXI1rU6Eus04ih2vU0hxXAibXXMzH1WlDZ8bXcTJmhwmTY8cP392+6l7cWeMnTQOibBUz8UKV3nhcCBAefw==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^1.0.0", @@ -5378,15 +5475,15 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.16.tgz", - "integrity": "sha512-nOINg/OUcZazCW7B55QV2/UB8QAqz9FYe4+z229+4RYboBTZ102K7ebOEjY5sKn59JgAkhjZTz+5BKmXpDFopw==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.4.tgz", + "integrity": "sha512-EHl6eNod/914xDRK4nu7gr78riK2cfi4DkAMvJt6COdaNGOnbR5eKrLe3SnRizyzzrPcxUMhflDL5hrcXS8rAQ==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.17" + "@swc/types": "^0.1.19" }, "engines": { "node": ">=10" @@ -5396,16 +5493,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.10.16", - "@swc/core-darwin-x64": "1.10.16", - "@swc/core-linux-arm-gnueabihf": "1.10.16", - "@swc/core-linux-arm64-gnu": "1.10.16", - "@swc/core-linux-arm64-musl": "1.10.16", - "@swc/core-linux-x64-gnu": "1.10.16", - "@swc/core-linux-x64-musl": "1.10.16", - "@swc/core-win32-arm64-msvc": "1.10.16", - "@swc/core-win32-ia32-msvc": "1.10.16", - "@swc/core-win32-x64-msvc": "1.10.16" + "@swc/core-darwin-arm64": "1.11.4", + "@swc/core-darwin-x64": "1.11.4", + "@swc/core-linux-arm-gnueabihf": "1.11.4", + "@swc/core-linux-arm64-gnu": "1.11.4", + "@swc/core-linux-arm64-musl": "1.11.4", + "@swc/core-linux-x64-gnu": "1.11.4", + "@swc/core-linux-x64-musl": "1.11.4", + "@swc/core-win32-arm64-msvc": "1.11.4", + "@swc/core-win32-ia32-msvc": "1.11.4", + "@swc/core-win32-x64-msvc": "1.11.4" }, "peerDependencies": { "@swc/helpers": "*" @@ -5417,9 +5514,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.16.tgz", - "integrity": "sha512-iikIxwqCQ4Bvz79vJ4ELh26efPf1u5D9TFdmXSJUBs7C3mmMHvk5zyWD9A9cTowXiW6WHs2gE58U1R9HOTTIcg==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.4.tgz", + "integrity": "sha512-Oi4lt4wqjpp80pcCh+vzvpsESJ8XXozYCE5EM/dDpr+9m2oRpkseds7Gq4ulzgdbUDPo1jJ1PonjjrKpfKY+sQ==", "cpu": [ "arm64" ], @@ -5434,9 +5531,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.16.tgz", - "integrity": "sha512-R2Eb9aktWd62vPfW9H/c/OaQ0e94iURibBo4uzUUcgxNNmB4+wb6piKbHxGdr/5bEsT+vJ1lwZFSRzfb45E7DA==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.4.tgz", + "integrity": "sha512-Tb7ez94DXxhX5iJ5slnAlT2gwJinQk3pMnQ46Npi6adKr3ZXM5Bdk0jpRUp8XjEcgNXkQRV1DtrySgCz6YlEnQ==", "cpu": [ "x64" ], @@ -5451,9 +5548,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.16.tgz", - "integrity": "sha512-mkqN3HBAMnuiSGZ/k2utScuH8rAPshvNj0T1LjBWon+X9DkMNHSA+aMLdWsy0yZKF1zjOPc4L3Uq2l2wzhUlzA==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.4.tgz", + "integrity": "sha512-p1uV+6Mi+0M+1kL7qL206ZaohomYMW7yroXSLDTJXbIylx7wG2xrUQL6AFtz2DwqDoX/E8jMNBjp+GcEy8r8Ig==", "cpu": [ "arm" ], @@ -5468,9 +5565,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.16.tgz", - "integrity": "sha512-PH/+q/L5nVZJ91CU07CL6Q9Whs6iR6nneMZMAgtVF9Ix8ST0cWVItdUhs6D38kFklCFhaOrpHhS01HlMJ72vWw==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.4.tgz", + "integrity": "sha512-4ijX4bWf9oc7kWkT6xUhugVGzEJ7U9c7CHNmt/xhI/yWsQdfM11+HECqWh7ay3m+aaEoVdvTeU5gykeF5jSxDA==", "cpu": [ "arm64" ], @@ -5485,9 +5582,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.16.tgz", - "integrity": "sha512-1169+C9XbydKKc6Ec1XZxTGKtHjZHDIFn0r+Nqp/QSVwkORrOY1Vz2Hdu7tn/lWMg36ZkGePS+LnnyV67s/7yg==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.4.tgz", + "integrity": "sha512-XI+gOgcuSanejbAC5QXKTjNA3GUJi7bzHmeJbNhKpX9d349RdVwan0k9okHmhMBY7BywAg3LK0ovF9PmOLgMHg==", "cpu": [ "arm64" ], @@ -5502,9 +5599,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.16.tgz", - "integrity": "sha512-n2rV0XwkjoHn4MDJmpYp5RBrnyi94/6GsJVpbn6f+/eqSrZn3mh3dT7pdZc9zCN1Qp9eDHo+uI6e/wgvbL22uA==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.4.tgz", + "integrity": "sha512-wyD6noaCPFayKOvl9mTxuiQoEULAagGuO0od2VkW7h4HvlgpOAZNekZYX73WEP/b+WuePNHurZ9KGpom43IzmA==", "cpu": [ "x64" ], @@ -5519,9 +5616,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.16.tgz", - "integrity": "sha512-EevCpwreBrkPrJjQVIbiM81lK42ukNNSlBmrSRxxbx2V9VGmOd5qxX0cJBn0TRRSLIPi62BuMS76F9iYjqsjgg==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.4.tgz", + "integrity": "sha512-e2vG9gUF1BRX0BWqSEHop6u14l5BtV3VS2Pmr+oquc0Ycs/zj81xhYc3ML4ByK5OxDkAaKBWryAOKTLaJA/DVg==", "cpu": [ "x64" ], @@ -5536,9 +5633,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.16.tgz", - "integrity": "sha512-BvE7RWAnKJeELVQWLok6env5I4GUVBTZSvaSN/VPgxnTjF+4PsTeQptYx0xCYhp5QCv68wWYsBnZKuPDS+SBsw==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.4.tgz", + "integrity": "sha512-rm51iljNqjCA/41gxYameuyjX1ENaTlvdxmaoPPYeUDt6hfypG93IxMJJCewaeHN9XfNxqZU7d4cupNqk+8nng==", "cpu": [ "arm64" ], @@ -5553,9 +5650,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.16.tgz", - "integrity": "sha512-7Jf/7AeCgbLR/JsQgMJuacHIq4Jeie3knf6+mXxn8aCvRypsOTIEu0eh7j24SolOboxK1ijqJ86GyN1VA2Rebg==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.4.tgz", + "integrity": "sha512-PHy3N6zlyU8te7Umi0ggXNbcx2VUkwpE59PW9FQQy9MBZM1Qn+OEGnO/4KLWjGFABw+9CwIeaRYgq6uCi1ry6A==", "cpu": [ "ia32" ], @@ -5570,9 +5667,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.16.tgz", - "integrity": "sha512-p0blVm0R8bjaTtmW+FoPmLxLSQdRNbqhuWcR/8g80OzMSkka9mk5/J3kn/5JRVWh+MaR9LHRHZc1Q1L8zan13g==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.4.tgz", + "integrity": "sha512-0TiriDGl7Dr4ObfMBk07PS4Ql5hgQH0QnU3E8I+fbs45hqfwC5OrN47HOsXx4ZbEw8XYxp2NM8SGnVoTIm4J8w==", "cpu": [ "x64" ], @@ -5602,9 +5699,9 @@ } }, "node_modules/@swc/types": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", - "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", + "version": "0.1.19", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.19.tgz", + "integrity": "sha512-WkAZaAfj44kh/UFdAQcrMP1I0nwRqpt27u+08LMBYMqmQfwwMofYoMh/48NGkMMRfC4ynpfwRbJuu8ErfNloeA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -5692,9 +5789,9 @@ "license": "MIT" }, "node_modules/@types/aws-lambda": { - "version": "8.10.143", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.143.tgz", - "integrity": "sha512-u5vzlcR14ge/4pMTTMDQr3MF0wEe38B2F9o84uC4F43vN5DGTy63npRrB6jQhyt+C0lGv4ZfiRcRkqJoZuPnmg==", + "version": "8.10.147", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.147.tgz", + "integrity": "sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==", "license": "MIT" }, "node_modules/@types/bcrypt": { @@ -5719,18 +5816,18 @@ } }, "node_modules/@types/bunyan": { - "version": "1.8.9", - "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.9.tgz", - "integrity": "sha512-ZqS9JGpBxVOvsawzmVt30sP++gSQMTejCkIAQ3VdadOcRE8izTyW66hufvwLeH+YEGP6Js2AW7Gz+RMyvrEbmw==", + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/@types/bunyan/-/bunyan-1.8.11.tgz", + "integrity": "sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/connect": { - "version": "3.4.36", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.36.tgz", - "integrity": "sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==", + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "license": "MIT", "dependencies": { "@types/node": "*" @@ -5960,14 +6057,24 @@ } }, "node_modules/@types/node": { - "version": "22.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz", - "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==", + "version": "22.13.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", + "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/nodemailer": { "version": "6.4.17", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", @@ -6037,9 +6144,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.0.8", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.8.tgz", - "integrity": "sha512-9P/o1IGdfmQxrujGbIMDyYaaCykhLKc0NGCtYcECNUr9UAaDe4gwvV9bR6tvd5Br1SG0j+PBpbKr2UYY8CwqSw==", + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", "dev": true, "license": "MIT", "dependencies": { @@ -6196,17 +6303,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", - "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz", + "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/type-utils": "8.24.0", - "@typescript-eslint/utils": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/type-utils": "8.25.0", + "@typescript-eslint/utils": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -6226,16 +6333,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", - "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz", + "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/typescript-estree": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "engines": { @@ -6251,14 +6358,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", - "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz", + "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0" + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6269,14 +6376,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", - "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz", + "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/utils": "8.24.0", + "@typescript-eslint/typescript-estree": "8.25.0", + "@typescript-eslint/utils": "8.25.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -6293,9 +6400,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", - "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", + "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", "dev": true, "license": "MIT", "engines": { @@ -6307,14 +6414,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", - "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz", + "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6360,16 +6467,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", - "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz", + "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0" + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/typescript-estree": "8.25.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6384,13 +6491,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", - "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz", + "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -6415,9 +6522,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz", - "integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.7.tgz", + "integrity": "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6438,8 +6545,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.5", - "vitest": "3.0.5" + "@vitest/browser": "3.0.7", + "vitest": "3.0.7" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -6448,15 +6555,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", - "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, "funding": { @@ -6464,13 +6571,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", - "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", + "@vitest/spy": "3.0.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -6501,9 +6608,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", - "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", "dev": true, "license": "MIT", "dependencies": { @@ -6514,38 +6621,38 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", - "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.5", - "pathe": "^2.0.2" + "@vitest/utils": "3.0.7", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", - "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", + "@vitest/pretty-format": "3.0.7", "magic-string": "^0.30.17", - "pathe": "^2.0.2" + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", - "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", "dev": true, "license": "MIT", "dependencies": { @@ -6556,14 +6663,14 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", - "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", - "loupe": "^3.1.2", + "@vitest/pretty-format": "3.0.7", + "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, "funding": { @@ -6938,13 +7045,13 @@ } }, "node_modules/ansis": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.9.0.tgz", - "integrity": "sha512-PcDrVe15ldexeZMsVLBAzBwF2KhZgaU0R+CHxH+x5kqn/pO+UWVBZJ+NEXMPpEOLUFeNsnNdoWYc2gwO+MVkDg==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.16.0.tgz", + "integrity": "sha512-sU7d/tfZiYrsIAXbdL/CNZld5bCkruzwT5KmqmadCJYxuLxHAOBjidxD5+iLmN/6xEfjcQq1l7OpsiCBlc4LzA==", "dev": true, "license": "ISC", "engines": { - "node": ">=16" + "node": ">=14" } }, "node_modules/any-promise": { @@ -7131,7 +7238,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/b4a": { @@ -7676,9 +7782,9 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", "dependencies": { @@ -8063,7 +8169,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -8451,7 +8556,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -8491,6 +8595,26 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-europe-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz", + "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -8964,22 +9088,22 @@ } }, "node_modules/eslint": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", - "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", + "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.19.2", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.21.0", + "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -9024,9 +9148,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", - "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.2.tgz", + "integrity": "sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==", "dev": true, "license": "MIT", "bin": { @@ -9101,6 +9225,19 @@ "eslint": ">=8.56.0" } }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", @@ -9131,19 +9268,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", @@ -9784,7 +9908,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -10267,9 +10390,9 @@ } }, "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", "dev": true, "license": "MIT", "engines": { @@ -10487,9 +10610,9 @@ } }, "node_modules/i18n-iso-countries": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.13.0.tgz", - "integrity": "sha512-pVh4CjdgAHZswI98hzG+1BItQlsQfR+yGDsjDISoWIV/jHDAvCmSyZ5vj2YWwAjfVZ8/BhBDqWcFvuGOyHe4vg==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.14.0.tgz", + "integrity": "sha512-nXHJZYtNrfsi1UQbyRqm3Gou431elgLjKl//CYlnBGt5aTWdRPH1PiS2T/p/n8Q8LnqYqzQJik3Q7mkwvLokeg==", "license": "MIT", "dependencies": { "diacritics": "1.3.0" @@ -10785,6 +10908,26 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-standalone-pwa": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz", + "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -11953,9 +12096,9 @@ } }, "node_modules/nestjs-cls": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-5.2.0.tgz", - "integrity": "sha512-xabZQ7aPHttZ5TwC4rEyYgsxm3/ArM+Dz4oJPWc5Q1p+jOp+UaDe37fKna6sIMeUmYpvZxMVtUKIhv7CfLxbOw==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-5.4.0.tgz", + "integrity": "sha512-XkD1mxctTdtlzC+4LR+7amvCGfIWghRjRBG1twczERPDUETg7Mw6RsruQoF3QrL/LKaOpMmm5OsJesTHt+hWCA==", "license": "MIT", "engines": { "node": ">=18" @@ -12055,9 +12198,9 @@ "license": "MIT" }, "node_modules/node-addon-api": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.0.tgz", - "integrity": "sha512-8VOpLHFrOQlAH+qA0ZzuGRlALRA6/LVh8QJldbrC4DY0hXoMP0l4Acq8TzFC018HztWiRqyCEj2aTWY2UvnJUg==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", "dev": true, "license": "MIT", "engines": { @@ -12560,9 +12703,9 @@ } }, "node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -12966,9 +13109,9 @@ } }, "node_modules/prettier": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", - "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", + "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -14067,9 +14210,9 @@ } }, "node_modules/rxjs": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" @@ -14500,9 +14643,9 @@ "license": "MIT" }, "node_modules/sirv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.0.tgz", - "integrity": "sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.24", @@ -14704,9 +14847,9 @@ } }, "node_modules/sql-formatter": { - "version": "15.4.10", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.10.tgz", - "integrity": "sha512-zQfiuxU1F/C7TNu+880BdL+fuvJTd1Kj8R0wv48dfZ27NR3z1PWvQFkH8ai/HrIy+NyvXCaZBkJHp/EeZFXSOA==", + "version": "15.4.11", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.11.tgz", + "integrity": "sha512-AfIjH0mYxv0NVzs4mbcGIAcos2Si20LeF9GMk0VmVA4A3gs1PFIixVu3rtcz34ls7ghPAjrDb+XbRly/aF6HAg==", "dev": true, "license": "MIT", "dependencies": { @@ -14985,9 +15128,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", - "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.19.0.tgz", + "integrity": "sha512-bSVZeYaqanMFeW5ZY3+EejFbsjkjazYxm1I7Lz3xayYz5XU3m2aUzvuPC0jI95WCQdduszHYV3ER4buQoy8DXA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -15913,10 +16056,30 @@ "node": ">=14.17" } }, + "node_modules/ua-is-frozen": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz", + "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT" + }, "node_modules/ua-parser-js": { - "version": "1.0.40", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", - "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.2.tgz", + "integrity": "sha512-NoaPjzMmuUlo5bJ2jrdkzvHL8gpcgVrhUugAqsqsundDO3R8rw7R0OwxLoWhcKtsTb+6u3z9dES8m6+vxEcJog==", "funding": [ { "type": "opencollective", @@ -15931,7 +16094,14 @@ "url": "https://github.com/sponsors/faisalman" } ], - "license": "MIT", + "license": "AGPL-3.0-or-later", + "dependencies": { + "@types/node-fetch": "^2.6.12", + "detect-europe-js": "^0.1.2", + "is-standalone-pwa": "^0.1.1", + "node-fetch": "^2.7.0", + "ua-is-frozen": "^0.1.2" + }, "bin": { "ua-parser-js": "script/cli.js" }, @@ -16213,16 +16383,16 @@ } }, "node_modules/vite-node": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", - "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", - "pathe": "^2.0.2", + "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { @@ -16715,31 +16885,31 @@ } }, "node_modules/vitest": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", - "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.5", - "@vitest/mocker": "3.0.5", - "@vitest/pretty-format": "^3.0.5", - "@vitest/runner": "3.0.5", - "@vitest/snapshot": "3.0.5", - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", - "pathe": "^2.0.2", + "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.5", + "vite-node": "3.0.7", "why-is-node-running": "^2.3.0" }, "bin": { @@ -16755,8 +16925,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.5", - "@vitest/ui": "3.0.5", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", "happy-dom": "*", "jsdom": "*" }, @@ -16814,9 +16984,9 @@ "license": "BSD-2-Clause" }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.98.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", + "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "dev": true, "license": "MIT", "dependencies": { @@ -16838,9 +17008,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, @@ -16887,6 +17057,36 @@ "dev": true, "license": "MIT" }, + "node_modules/webpack/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -16911,6 +17111,33 @@ "node": ">=4.0" } }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/server/package.json b/server/package.json index fa0be7d4e4..052c2da93d 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.126.1", + "version": "1.129.0", "description": "", "author": "", "private": true, @@ -92,7 +92,7 @@ "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", - "ua-parser-js": "^1.0.35", + "ua-parser-js": "^2.0.0", "validator": "^13.12.0" }, "devDependencies": { @@ -114,7 +114,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/pngjs": "^6.0.5", @@ -130,7 +130,7 @@ "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^56.0.1", - "globals": "^15.9.0", + "globals": "^16.0.0", "kysely-codegen": "^0.17.0", "mock-fs": "^5.2.0", "node-addon-api": "^8.3.0", diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index 4765993643..a9f5d72ec9 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -6,6 +6,7 @@ import { Test } from '@nestjs/testing'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ClassConstructor } from 'class-transformer'; import { PostgresJSDialect } from 'kysely-postgres-js'; +import { ClsModule } from 'nestjs-cls'; import { KyselyModule } from 'nestjs-kysely'; import { OpenTelemetryModule } from 'nestjs-otel'; import { mkdir, rm, writeFile } from 'node:fs/promises'; @@ -77,7 +78,7 @@ class SqlGenerator { await mkdir(this.options.targetDir); process.env.DB_HOSTNAME = 'localhost'; - const { database, otel } = new ConfigRepository().getEnv(); + const { database, cls, otel } = new ConfigRepository().getEnv(); const moduleFixture = await Test.createTestingModule({ imports: [ @@ -92,6 +93,7 @@ class SqlGenerator { } }, }), + ClsModule.forRoot(cls.config), TypeOrmModule.forRoot({ ...database.config.typeorm, entities, diff --git a/server/src/constants.ts b/server/src/constants.ts index 889ce81620..20ce7dd497 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -38,6 +38,11 @@ export const ONE_HOUR = Duration.fromObject({ hours: 1 }); export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload'; +export const MACHINE_LEARNING_PING_TIMEOUT = Number(process.env.MACHINE_LEARNING_PING_TIMEOUT || 2000); +export const MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME = Number( + process.env.MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME || 30_000, +); + export const citiesFile = 'cities500.txt'; export const MOBILE_REDIRECT = 'app.immich:///oauth-callback'; diff --git a/server/src/database.ts b/server/src/database.ts index c3fb4cbab4..46d33d916d 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -52,5 +52,6 @@ export const columns = { 'shared_links.password', ], userDto: ['id', 'name', 'email', 'profileImagePath', 'profileChangedAt'], + tagDto: ['id', 'value', 'createdAt', 'updatedAt', 'color', 'parentId'], apiKey: ['id', 'name', 'userId', 'createdAt', 'updatedAt', 'permissions'], } as const; diff --git a/server/src/db.d.ts b/server/src/db.d.ts index 4a2adc917f..4c75562ba1 100644 --- a/server/src/db.d.ts +++ b/server/src/db.d.ts @@ -29,7 +29,7 @@ export type JsonPrimitive = boolean | number | string | null; export type JsonValue = JsonArray | JsonObject | JsonPrimitive; -export type Sourcetype = 'exif' | 'machine-learning'; +export type Sourcetype = 'exif' | 'machine-learning' | 'manual'; export type Timestamp = ColumnType; @@ -41,6 +41,7 @@ export interface Activity { id: Generated; isLiked: Generated; updatedAt: Generated; + updateId: Generated; userId: string; } @@ -58,6 +59,7 @@ export interface Albums { order: Generated; ownerId: string; updatedAt: Generated; + updateId: Generated; } export interface AlbumsAssetsAssets { @@ -79,6 +81,7 @@ export interface ApiKeys { name: string; permissions: Permission[]; updatedAt: Generated; + updateId: Generated; userId: string; } @@ -103,6 +106,7 @@ export interface AssetFiles { path: string; type: string; updatedAt: Generated; + updateId: Generated; } export interface AssetJobStatus { @@ -143,6 +147,7 @@ export interface Assets { thumbhash: Buffer | null; type: string; updatedAt: Generated; + updateId: Generated; } export interface AssetStack { @@ -221,6 +226,7 @@ export interface Libraries { ownerId: string; refreshedAt: Timestamp | null; updatedAt: Generated; + updateId: Generated; } export interface Memories { @@ -236,6 +242,7 @@ export interface Memories { showAt: Timestamp | null; type: string; updatedAt: Generated; + updateId: Generated; } export interface MemoriesAssetsAssets { @@ -265,12 +272,20 @@ export interface NaturalearthCountries { type: string; } +export interface PartnersAudit { + deletedAt: Generated; + id: Generated; + sharedById: string; + sharedWithId: string; +} + export interface Partners { createdAt: Generated; inTimeline: Generated; sharedById: string; sharedWithId: string; updatedAt: Generated; + updateId: Generated; } export interface Person { @@ -285,6 +300,7 @@ export interface Person { ownerId: string; thumbnailPath: Generated; updatedAt: Generated; + updateId: Generated; } export interface Sessions { @@ -294,6 +310,7 @@ export interface Sessions { id: Generated; token: string; updatedAt: Generated; + updateId: Generated; userId: string; } @@ -303,9 +320,9 @@ export interface SessionSyncCheckpoints { sessionId: string; type: SyncEntityType; updatedAt: Generated; + updateId: Generated; } - export interface SharedLinkAsset { assetsId: string; sharedLinksId: string; @@ -358,6 +375,7 @@ export interface Tags { id: Generated; parentId: string | null; updatedAt: Generated; + updateId: Generated; userId: string; value: string; } @@ -399,9 +417,11 @@ export interface Users { status: Generated; storageLabel: string | null; updatedAt: Generated; + updateId: Generated; } export interface UsersAudit { + id: Generated; userId: string; deletedAt: Generated; } @@ -448,6 +468,7 @@ export interface DB { migrations: Migrations; move_history: MoveHistory; naturalearth_countries: NaturalearthCountries; + partners_audit: PartnersAudit; partners: Partners; person: Person; sessions: Sessions; diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 0628a566cd..d191c82bb3 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -45,15 +45,30 @@ export class SyncUserDeleteV1 { userId!: string; } +export class SyncPartnerV1 { + sharedById!: string; + sharedWithId!: string; + inTimeline!: boolean; +} + +export class SyncPartnerDeleteV1 { + sharedById!: string; + sharedWithId!: string; +} + export type SyncItem = { [SyncEntityType.UserV1]: SyncUserV1; [SyncEntityType.UserDeleteV1]: SyncUserDeleteV1; + [SyncEntityType.PartnerV1]: SyncPartnerV1; + [SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1; }; const responseDtos = [ // SyncUserV1, SyncUserDeleteV1, + SyncPartnerV1, + SyncPartnerDeleteV1, ]; export const extraSyncModels = responseDtos; diff --git a/server/src/dtos/tag.dto.ts b/server/src/dtos/tag.dto.ts index 17200a8874..e62cf21636 100644 --- a/server/src/dtos/tag.dto.ts +++ b/server/src/dtos/tag.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsHexColor, IsNotEmpty, IsString } from 'class-validator'; import { TagEntity } from 'src/entities/tag.entity'; +import { TagItem } from 'src/types'; import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation'; export class TagCreateDto { @@ -51,7 +52,7 @@ export class TagResponseDto { color?: string; } -export function mapTag(entity: TagEntity): TagResponseDto { +export function mapTag(entity: TagItem | TagEntity): TagResponseDto { return { id: entity.id, parentId: entity.parentId ?? undefined, diff --git a/server/src/entities/activity.entity.ts b/server/src/entities/activity.entity.ts index 8de76ac894..dabb371977 100644 --- a/server/src/entities/activity.entity.ts +++ b/server/src/entities/activity.entity.ts @@ -25,6 +25,10 @@ export class ActivityEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_activity_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @Column() albumId!: string; diff --git a/server/src/entities/album.entity.ts b/server/src/entities/album.entity.ts index 5aec5a0f47..4cd7c82394 100644 --- a/server/src/entities/album.entity.ts +++ b/server/src/entities/album.entity.ts @@ -8,6 +8,7 @@ import { CreateDateColumn, DeleteDateColumn, Entity, + Index, JoinTable, ManyToMany, ManyToOne, @@ -39,6 +40,10 @@ export class AlbumEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_albums_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @DeleteDateColumn({ type: 'timestamptz' }) deletedAt!: Date | null; diff --git a/server/src/entities/api-key.entity.ts b/server/src/entities/api-key.entity.ts index 998ee4f8ef..f59bf0d918 100644 --- a/server/src/entities/api-key.entity.ts +++ b/server/src/entities/api-key.entity.ts @@ -1,6 +1,6 @@ import { UserEntity } from 'src/entities/user.entity'; import { Permission } from 'src/enum'; -import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; @Entity('api_keys') export class APIKeyEntity { @@ -27,4 +27,8 @@ export class APIKeyEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + + @Index('IDX_api_keys_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; } diff --git a/server/src/entities/asset-files.entity.ts b/server/src/entities/asset-files.entity.ts index a8a6ddfee1..09f96e849d 100644 --- a/server/src/entities/asset-files.entity.ts +++ b/server/src/entities/asset-files.entity.ts @@ -30,6 +30,10 @@ export class AssetFileEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_asset_files_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @Column() type!: AssetFileType; diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index a325febce7..b2589e1231 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -96,6 +96,10 @@ export class AssetEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_assets_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @DeleteDateColumn({ type: 'timestamptz', nullable: true }) deletedAt!: Date | null; @@ -256,6 +260,7 @@ export function hasPeople(qb: SelectQueryBuilder, personIds: .selectFrom('asset_faces') .select('assetId') .where('personId', '=', anyUuid(personIds!)) + .where('deletedAt', 'is', null) .groupBy('assetId') .having((eb) => eb.fn.count('personId').distinct(), '=', personIds.length) .as('has_people'), @@ -347,7 +352,7 @@ const joinDeduplicationPlugin = new DeduplicateJoinsPlugin(); /** TODO: This should only be used for search-related queries, not as a general purpose query builder */ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuilderOptions) { options.isArchived ??= options.withArchived ? undefined : false; - options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore); + options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline); return kysely .withPlugin(joinDeduplicationPlugin) .selectFrom('assets') diff --git a/server/src/entities/library.entity.ts b/server/src/entities/library.entity.ts index a6053e4213..a594fd83ad 100644 --- a/server/src/entities/library.entity.ts +++ b/server/src/entities/library.entity.ts @@ -5,6 +5,7 @@ import { CreateDateColumn, DeleteDateColumn, Entity, + Index, JoinTable, ManyToOne, OneToMany, @@ -42,6 +43,10 @@ export class LibraryEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_libraries_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @DeleteDateColumn({ type: 'timestamptz' }) deletedAt?: Date; diff --git a/server/src/entities/memory.entity.ts b/server/src/entities/memory.entity.ts index 1f53d7a5c1..dafd7eb21c 100644 --- a/server/src/entities/memory.entity.ts +++ b/server/src/entities/memory.entity.ts @@ -6,6 +6,7 @@ import { CreateDateColumn, DeleteDateColumn, Entity, + Index, JoinTable, ManyToMany, ManyToOne, @@ -30,6 +31,10 @@ export class MemoryEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_memories_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @DeleteDateColumn({ type: 'timestamptz' }) deletedAt?: Date; diff --git a/server/src/entities/move.entity.ts b/server/src/entities/move.entity.ts index 5cdef5d22e..7a998eaebe 100644 --- a/server/src/entities/move.entity.ts +++ b/server/src/entities/move.entity.ts @@ -10,7 +10,7 @@ export class MoveEntity { @PrimaryGeneratedColumn('uuid') id!: string; - @Column({ type: 'varchar' }) + @Column({ type: 'uuid' }) entityId!: string; @Column({ type: 'varchar' }) diff --git a/server/src/entities/partner-audit.entity.ts b/server/src/entities/partner-audit.entity.ts new file mode 100644 index 0000000000..a731e017dc --- /dev/null +++ b/server/src/entities/partner-audit.entity.ts @@ -0,0 +1,19 @@ +import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm'; + +@Entity('partners_audit') +export class PartnerAuditEntity { + @PrimaryColumn({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + id!: string; + + @Index('IDX_partners_audit_shared_by_id') + @Column({ type: 'uuid' }) + sharedById!: string; + + @Index('IDX_partners_audit_shared_with_id') + @Column({ type: 'uuid' }) + sharedWithId!: string; + + @Index('IDX_partners_audit_deleted_at') + @CreateDateColumn({ type: 'timestamptz', default: () => 'clock_timestamp()' }) + deletedAt!: Date; +} diff --git a/server/src/entities/partner.entity.ts b/server/src/entities/partner.entity.ts index 189f6f51a7..877330a8e7 100644 --- a/server/src/entities/partner.entity.ts +++ b/server/src/entities/partner.entity.ts @@ -1,5 +1,14 @@ import { UserEntity } from 'src/entities/user.entity'; -import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm'; +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryColumn, + UpdateDateColumn, +} from 'typeorm'; @Entity('partners') export class PartnerEntity { @@ -23,6 +32,10 @@ export class PartnerEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_partners_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @Column({ type: 'boolean', default: false }) inTimeline!: boolean; } diff --git a/server/src/entities/person.entity.ts b/server/src/entities/person.entity.ts index 3785e1985e..5ca74c12d2 100644 --- a/server/src/entities/person.entity.ts +++ b/server/src/entities/person.entity.ts @@ -5,6 +5,7 @@ import { Column, CreateDateColumn, Entity, + Index, ManyToOne, OneToMany, PrimaryGeneratedColumn, @@ -23,6 +24,10 @@ export class PersonEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_person_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @Column() ownerId!: string; diff --git a/server/src/entities/session.entity.ts b/server/src/entities/session.entity.ts index e21c6d52ba..cb208c958e 100644 --- a/server/src/entities/session.entity.ts +++ b/server/src/entities/session.entity.ts @@ -1,7 +1,7 @@ import { ExpressionBuilder } from 'kysely'; import { DB } from 'src/db'; import { UserEntity } from 'src/entities/user.entity'; -import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; +import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; @Entity('sessions') export class SessionEntity { @@ -23,6 +23,10 @@ export class SessionEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_sessions_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId!: string; + @Column({ default: '' }) deviceType!: string; diff --git a/server/src/entities/sync-checkpoint.entity.ts b/server/src/entities/sync-checkpoint.entity.ts index 2a91d2386c..7c6818aba0 100644 --- a/server/src/entities/sync-checkpoint.entity.ts +++ b/server/src/entities/sync-checkpoint.entity.ts @@ -1,6 +1,6 @@ import { SessionEntity } from 'src/entities/session.entity'; import { SyncEntityType } from 'src/enum'; -import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm'; +import { Column, CreateDateColumn, Entity, Index, ManyToOne, PrimaryColumn, UpdateDateColumn } from 'typeorm'; @Entity('session_sync_checkpoints') export class SessionSyncCheckpointEntity { @@ -19,6 +19,10 @@ export class SessionSyncCheckpointEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_session_sync_checkpoints_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @Column() ack!: string; } diff --git a/server/src/entities/tag.entity.ts b/server/src/entities/tag.entity.ts index ebcc6853c9..fcbde6c779 100644 --- a/server/src/entities/tag.entity.ts +++ b/server/src/entities/tag.entity.ts @@ -4,6 +4,7 @@ import { Column, CreateDateColumn, Entity, + Index, ManyToMany, ManyToOne, PrimaryGeneratedColumn, @@ -30,6 +31,10 @@ export class TagEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_tags_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @Column({ type: 'varchar', nullable: true, default: null }) color!: string | null; diff --git a/server/src/entities/user-audit.entity.ts b/server/src/entities/user-audit.entity.ts index 305994a6d6..c29bc94d97 100644 --- a/server/src/entities/user-audit.entity.ts +++ b/server/src/entities/user-audit.entity.ts @@ -1,14 +1,14 @@ -import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, CreateDateColumn, Entity, Index, PrimaryColumn } from 'typeorm'; @Entity('users_audit') -@Index('IDX_users_audit_deleted_at_asc_user_id_asc', ['deletedAt', 'userId']) export class UserAuditEntity { - @PrimaryGeneratedColumn('increment') - id!: number; + @PrimaryColumn({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + id!: string; @Column({ type: 'uuid' }) userId!: string; - @CreateDateColumn({ type: 'timestamptz' }) + @Index('IDX_users_audit_deleted_at') + @CreateDateColumn({ type: 'timestamptz', default: () => 'clock_timestamp()' }) deletedAt!: Date; } diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index b597d15cf9..5758e29098 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -58,6 +58,10 @@ export class UserEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt!: Date; + @Index('IDX_users_update_id') + @Column({ type: 'uuid', nullable: false, default: () => 'immich_uuid_v7()' }) + updateId?: string; + @OneToMany(() => TagEntity, (tag) => tag.user) tags!: TagEntity[]; diff --git a/server/src/enum.ts b/server/src/enum.ts index 7bf4ca3dcf..b9a56144fb 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -228,6 +228,7 @@ export enum AssetStatus { export enum SourceType { MACHINE_LEARNING = 'machine-learning', EXIF = 'exif', + MANUAL = 'manual', } export enum ManualJobName { @@ -436,7 +437,6 @@ export enum JobName { // metadata QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction', METADATA_EXTRACTION = 'metadata-extraction', - LINK_LIVE_PHOTOS = 'link-live-photos', // user USER_DELETION = 'user-deletion', @@ -472,7 +472,7 @@ export enum JobName { LIBRARY_SYNC_FILE = 'library-sync-file', LIBRARY_SYNC_ASSET = 'library-sync-asset', LIBRARY_DELETE = 'library-delete', - LIBRARY_QUEUE_SYNC_ALL = 'library-queue-sync-all', + LIBRARY_QUEUE_SCAN_ALL = 'library-queue-scan-all', LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', // cleanup @@ -547,9 +547,12 @@ export enum DatabaseLock { export enum SyncRequestType { UsersV1 = 'UsersV1', + PartnersV1 = 'PartnersV1', } export enum SyncEntityType { UserV1 = 'UserV1', UserDeleteV1 = 'UserDeleteV1', + PartnerV1 = 'PartnerV1', + PartnerDeleteV1 = 'PartnerDeleteV1', } diff --git a/server/src/migrations/1740586617223-AddUpdateIdColumns.ts b/server/src/migrations/1740586617223-AddUpdateIdColumns.ts new file mode 100644 index 0000000000..02d680ddf6 --- /dev/null +++ b/server/src/migrations/1740586617223-AddUpdateIdColumns.ts @@ -0,0 +1,134 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddUpdateIdColumns1740586617223 implements MigrationInterface { + name = 'AddUpdateIdColumns1740586617223' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + create or replace function immich_uuid_v7(p_timestamp timestamp with time zone default clock_timestamp()) + returns uuid + as $$ + select encode( + set_bit( + set_bit( + overlay(uuid_send(gen_random_uuid()) + placing substring(int8send(floor(extract(epoch from p_timestamp) * 1000)::bigint) from 3) + from 1 for 6 + ), + 52, 1 + ), + 53, 1 + ), + 'hex')::uuid; + $$ + language SQL + volatile; + `) + await queryRunner.query(` + CREATE OR REPLACE FUNCTION updated_at() RETURNS TRIGGER + LANGUAGE plpgsql + as $$ + BEGIN + return new; + END; + $$; + `) + await queryRunner.query(`ALTER TABLE "person" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "asset_files" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "libraries" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "tags" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "assets" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "users" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "albums" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "sessions" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "session_sync_checkpoints" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "partners" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "memories" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "api_keys" ADD "updateId" uuid`); + await queryRunner.query(`ALTER TABLE "activity" ADD "updateId" uuid`); + + await queryRunner.query(`UPDATE "person" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "asset_files" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "libraries" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "tags" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "assets" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "users" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "albums" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "sessions" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "session_sync_checkpoints" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "partners" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "memories" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "api_keys" SET "updateId" = immich_uuid_v7("updatedAt")`); + await queryRunner.query(`UPDATE "activity" SET "updateId" = immich_uuid_v7("updatedAt")`); + + await queryRunner.query(`ALTER TABLE "person" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "asset_files" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "libraries" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "tags" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "albums" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "sessions" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "session_sync_checkpoints" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "partners" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "memories" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "api_keys" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "activity" ALTER COLUMN "updateId" SET NOT NULL, ALTER COLUMN "updateId" SET DEFAULT immich_uuid_v7()`); + + await queryRunner.query(`CREATE INDEX "IDX_person_update_id" ON "person" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_asset_files_update_id" ON "asset_files" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_libraries_update_id" ON "libraries" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_tags_update_id" ON "tags" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_assets_update_id" ON "assets" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_users_update_id" ON "users" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_albums_update_id" ON "albums" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_sessions_update_id" ON "sessions" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_session_sync_checkpoints_update_id" ON "session_sync_checkpoints" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_partners_update_id" ON "partners" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_memories_update_id" ON "memories" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_api_keys_update_id" ON "api_keys" ("updateId")`); + await queryRunner.query(`CREATE INDEX "IDX_activity_update_id" ON "activity" ("updateId")`); + + await queryRunner.query(` + CREATE OR REPLACE FUNCTION updated_at() RETURNS TRIGGER + LANGUAGE plpgsql + as $$ + DECLARE + clock_timestamp TIMESTAMP := clock_timestamp(); + BEGIN + new."updatedAt" = clock_timestamp; + new."updateId" = immich_uuid_v7(clock_timestamp); + return new; + END; + $$; + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "activity" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "api_keys" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "memories" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "partners" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "session_sync_checkpoints" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "libraries" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "asset_files" DROP COLUMN "updateId"`); + await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "updateId"`); + await queryRunner.query(`DROP FUNCTION immich_uuid_v7`); + await queryRunner.query(` + CREATE OR REPLACE FUNCTION updated_at() RETURNS TRIGGER + LANGUAGE plpgsql + as $$ + BEGIN + new."updatedAt" = now(); + return new; + END; + $$; + `) + } + +} diff --git a/server/src/migrations/1740595460866-UsersAuditUuidv7PrimaryKey.ts b/server/src/migrations/1740595460866-UsersAuditUuidv7PrimaryKey.ts new file mode 100644 index 0000000000..59fc4dbd5b --- /dev/null +++ b/server/src/migrations/1740595460866-UsersAuditUuidv7PrimaryKey.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class UsersAuditUuidv7PrimaryKey1740595460866 implements MigrationInterface { + name = 'UsersAuditUuidv7PrimaryKey1740595460866' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_users_audit_deleted_at_asc_user_id_asc"`); + await queryRunner.query(`ALTER TABLE "users_audit" DROP CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180"`); + await queryRunner.query(`ALTER TABLE "users_audit" DROP COLUMN "id"`); + await queryRunner.query(`ALTER TABLE "users_audit" ADD "id" uuid NOT NULL DEFAULT immich_uuid_v7()`); + await queryRunner.query(`ALTER TABLE "users_audit" ADD CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180" PRIMARY KEY ("id")`); + await queryRunner.query(`ALTER TABLE "users_audit" ALTER COLUMN "deletedAt" SET DEFAULT clock_timestamp()`) + await queryRunner.query(`CREATE INDEX "IDX_users_audit_deleted_at" ON "users_audit" ("deletedAt")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_users_audit_deleted_at"`); + await queryRunner.query(`ALTER TABLE "users_audit" DROP CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180"`); + await queryRunner.query(`ALTER TABLE "users_audit" DROP COLUMN "id"`); + await queryRunner.query(`ALTER TABLE "users_audit" ADD "id" SERIAL NOT NULL`); + await queryRunner.query(`ALTER TABLE "users_audit" ADD CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180" PRIMARY KEY ("id")`); + await queryRunner.query(`ALTER TABLE "users_audit" ALTER COLUMN "deletedAt" SET DEFAULT now()`); + await queryRunner.query(`CREATE INDEX "IDX_users_audit_deleted_at_asc_user_id_asc" ON "users_audit" ("userId", "deletedAt") `); + } + +} diff --git a/server/src/migrations/1740619600996-AddManualSourceType.ts b/server/src/migrations/1740619600996-AddManualSourceType.ts new file mode 100644 index 0000000000..dd53312ad7 --- /dev/null +++ b/server/src/migrations/1740619600996-AddManualSourceType.ts @@ -0,0 +1,27 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddManualSourceType1740619600996 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TYPE sourceType ADD VALUE 'manual'`); + } + + public async down(queryRunner: QueryRunner): Promise { + // Prior to this migration, manually tagged pictures had the 'machine-learning' type + await queryRunner.query( + `UPDATE "asset_faces" SET "sourceType" = 'machine-learning' WHERE "sourceType" = 'manual';`, + ); + + // Postgres doesn't allow removing values from enums, we have to recreate the type + await queryRunner.query(`ALTER TYPE sourceType RENAME TO oldSourceType`); + await queryRunner.query(`CREATE TYPE sourceType AS ENUM ('machine-learning', 'exif');`); + + await queryRunner.query(`ALTER TABLE "asset_faces" ALTER COLUMN "sourceType" DROP DEFAULT;`); + await queryRunner.query( + `ALTER TABLE "asset_faces" ALTER COLUMN "sourceType" TYPE sourceType USING "sourceType"::text::sourceType;`, + ); + await queryRunner.query( + `ALTER TABLE "asset_faces" ALTER COLUMN "sourceType" SET DEFAULT 'machine-learning'::sourceType;`, + ); + await queryRunner.query(`DROP TYPE oldSourceType;`); + } +} diff --git a/server/src/migrations/1740654480319-UnsetStackedAssetsFromDuplicateStatus.ts b/server/src/migrations/1740654480319-UnsetStackedAssetsFromDuplicateStatus.ts new file mode 100644 index 0000000000..5c735a60bb --- /dev/null +++ b/server/src/migrations/1740654480319-UnsetStackedAssetsFromDuplicateStatus.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UnsetStackedAssetsFromDuplicateStatus1740654480319 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + update assets + set "duplicateId" = null + where "stackId" is not null`); + } + + public async down(): Promise { + // No need to revert this migration + } +} diff --git a/server/src/migrations/1740739778549-CreatePartnersAuditTable.ts b/server/src/migrations/1740739778549-CreatePartnersAuditTable.ts new file mode 100644 index 0000000000..d9c9dc1949 --- /dev/null +++ b/server/src/migrations/1740739778549-CreatePartnersAuditTable.ts @@ -0,0 +1,38 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CreatePartnersAuditTable1740739778549 implements MigrationInterface { + name = 'CreatePartnersAuditTable1740739778549' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "partners_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "deletedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT clock_timestamp(), CONSTRAINT "PK_952b50217ff78198a7e380f0359" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_partners_audit_shared_by_id" ON "partners_audit" ("sharedById") `); + await queryRunner.query(`CREATE INDEX "IDX_partners_audit_shared_with_id" ON "partners_audit" ("sharedWithId") `); + await queryRunner.query(`CREATE INDEX "IDX_partners_audit_deleted_at" ON "partners_audit" ("deletedAt") `); + await queryRunner.query(`CREATE OR REPLACE FUNCTION partners_delete_audit() RETURNS TRIGGER AS + $$ + BEGIN + INSERT INTO partners_audit ("sharedById", "sharedWithId") + SELECT "sharedById", "sharedWithId" + FROM OLD; + RETURN NULL; + END; + $$ LANGUAGE plpgsql` + ); + await queryRunner.query(`CREATE OR REPLACE TRIGGER partners_delete_audit + AFTER DELETE ON partners + REFERENCING OLD TABLE AS OLD + FOR EACH STATEMENT + EXECUTE FUNCTION partners_delete_audit(); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_partners_audit_deleted_at"`); + await queryRunner.query(`DROP INDEX "public"."IDX_partners_audit_shared_with_id"`); + await queryRunner.query(`DROP INDEX "public"."IDX_partners_audit_shared_by_id"`); + await queryRunner.query(`DROP TRIGGER partners_delete_audit`); + await queryRunner.query(`DROP FUNCTION partners_delete_audit`); + await queryRunner.query(`DROP TABLE "partners_audit"`); + } + +} diff --git a/server/src/migrations/1741027685381-ResetMemories.ts b/server/src/migrations/1741027685381-ResetMemories.ts new file mode 100644 index 0000000000..6a80372219 --- /dev/null +++ b/server/src/migrations/1741027685381-ResetMemories.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ResetMemories1741027685381 implements MigrationInterface { + name = 'ResetMemories1741027685381'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DELETE FROM "memories"`); + await queryRunner.query(`DELETE FROM "system_metadata" WHERE "key" = 'memories-state'`); + } + + public async down(): Promise { + // nothing to do + } +} diff --git a/server/src/migrations/1741179334403-MoveHistoryUuidEntityId.ts b/server/src/migrations/1741179334403-MoveHistoryUuidEntityId.ts new file mode 100644 index 0000000000..449272341c --- /dev/null +++ b/server/src/migrations/1741179334403-MoveHistoryUuidEntityId.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MoveHistoryUuidEntityId1741179334403 implements MigrationInterface { + name = 'MoveHistoryUuidEntityId1741179334403'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "move_history" ALTER COLUMN "entityId" TYPE uuid USING "entityId"::uuid;`); + await queryRunner.query(`delete from "move_history" + where + "move_history"."entityId" not in ( + select + "id" + from + "assets" + where + "assets"."id" = "move_history"."entityId" + ) + and "move_history"."pathType" = 'original' + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "move_history" ALTER COLUMN "entityId" TYPE character varying`); + } +} + diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 879152dc77..c0b778bb50 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -55,9 +55,10 @@ with inner join "exif" on "a"."id" = "exif"."assetId" ) select - ( - (now() at time zone 'UTC')::date - ("localDateTime" at time zone 'UTC')::date - ) / 365 as "yearsAgo", + date_part( + 'year', + ("localDateTime" at time zone 'UTC')::date + )::int as "year", json_agg("res") as "assets" from "res" @@ -333,6 +334,7 @@ with and "assets"."duplicateId" is not null and "assets"."deletedAt" is null and "assets"."isVisible" = $2 + and "assets"."stackId" is null group by "assets"."duplicateId" ), diff --git a/server/src/queries/move.repository.sql b/server/src/queries/move.repository.sql index e51f2829df..a65c7a8b85 100644 --- a/server/src/queries/move.repository.sql +++ b/server/src/queries/move.repository.sql @@ -15,3 +15,22 @@ where "id" = $1 returning * + +-- MoveRepository.cleanMoveHistory +delete from "move_history" +where + "move_history"."entityId" not in ( + select + "id" + from + "assets" + where + "assets"."id" = "move_history"."entityId" + ) + and "move_history"."pathType" = 'original' + +-- MoveRepository.cleanMoveHistorySingle +delete from "move_history" +where + "move_history"."pathType" = 'original' + and "entityId" = $1 diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 9400700e56..06590dc817 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -112,6 +112,7 @@ with and "assets"."isVisible" = $3 and "assets"."type" = $4 and "assets"."id" != $5::uuid + and "assets"."stackId" is null order by smart_search.embedding <=> $6 limit diff --git a/server/src/queries/tag.repository.sql b/server/src/queries/tag.repository.sql index 580344c597..6c97d7843f 100644 --- a/server/src/queries/tag.repository.sql +++ b/server/src/queries/tag.repository.sql @@ -1,10 +1,118 @@ -- NOTE: This file is auto generated by ./sql-generator --- TagRepository.getAssetIds -SELECT - "tag_asset"."assetsId" AS "assetId" -FROM - "tag_asset" "tag_asset" -WHERE - "tag_asset"."tagsId" = $1 - AND "tag_asset"."assetsId" IN ($2) +-- TagRepository.get +select + "id", + "value", + "createdAt", + "updatedAt", + "color", + "parentId" +from + "tags" +where + "id" = $1 + +-- TagRepository.getByValue +select + "id", + "value", + "createdAt", + "updatedAt", + "color", + "parentId" +from + "tags" +where + "userId" = $1 + and "value" = $2 + +-- TagRepository.upsertValue +begin +insert into + "tags" ("userId", "value", "parentId") +values + ($1, $2, $3) +on conflict ("userId", "value") do update +set + "parentId" = $4 +returning + * +rollback + +-- TagRepository.getAll +select + "id", + "value", + "createdAt", + "updatedAt", + "color", + "parentId" +from + "tags" +where + "userId" = $1 +order by + "value" asc + +-- TagRepository.create +insert into + "tags" ("userId", "color", "value") +values + ($1, $2, $3) +returning + * + +-- TagRepository.update +update "tags" +set + "color" = $1 +where + "id" = $2 +returning + * + +-- TagRepository.delete +delete from "tags" +where + "id" = $1 + +-- TagRepository.addAssetIds +insert into + "tag_asset" ("tagsId", "assetsId") +values + ($1, $2) + +-- TagRepository.removeAssetIds +delete from "tag_asset" +where + "tagsId" = $1 + and "assetsId" in ($2) + +-- TagRepository.replaceAssetTags +begin +delete from "tag_asset" +where + "assetsId" = $1 +insert into + "tag_asset" ("tagsId", "assetsId") +values + ($1, $2) +on conflict do nothing +returning + * +rollback + +-- TagRepository.deleteEmptyTags +begin +select + "tags"."id", + count("assets"."id") as "count" +from + "assets" + inner join "tag_asset" on "tag_asset"."assetsId" = "assets"."id" + inner join "tags_closure" on "tags_closure"."id_descendant" = "tag_asset"."tagsId" + inner join "tags" on "tags"."id" = "tags_closure"."id_descendant" +group by + "tags"."id" +commit diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 139e652f03..91597ed720 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -192,7 +192,7 @@ export class AssetRepository { } @GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] }) - getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise { + getByDayOfYear(ownerIds: string[], { day, month }: MonthDay) { return this.db .with('res', (qb) => qb @@ -239,16 +239,12 @@ export class AssetRepository { .select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo')), ) .selectFrom('res') - .select( - sql`((now() at time zone 'UTC')::date - ("localDateTime" at time zone 'UTC')::date) / 365`.as( - 'yearsAgo', - ), - ) + .select(sql`date_part('year', ("localDateTime" at time zone 'UTC')::date)::int`.as('year')) .select((eb) => eb.fn.jsonAgg(eb.table('res')).as('assets')) .groupBy(sql`("localDateTime" at time zone 'UTC')::date`) .orderBy(sql`("localDateTime" at time zone 'UTC')::date`, 'desc') .limit(10) - .execute() as any as Promise; + .execute(); } @GenerateSql({ params: [[DummyValue.UUID]] }) @@ -794,6 +790,7 @@ export class AssetRepository { .where('assets.duplicateId', 'is not', null) .where('assets.deletedAt', 'is', null) .where('assets.isVisible', '=', true) + .where('assets.stackId', 'is', null) .groupBy('assets.duplicateId'), ) .with('unique', (qb) => diff --git a/server/src/repositories/download.repository.ts b/server/src/repositories/download.repository.ts new file mode 100644 index 0000000000..c9c62c90ce --- /dev/null +++ b/server/src/repositories/download.repository.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { Kysely } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { DB } from 'src/db'; +import { anyUuid } from 'src/utils/database'; + +const builder = (db: Kysely) => + db + .selectFrom('assets') + .innerJoin('exif', 'assetId', 'id') + .select(['assets.id', 'assets.livePhotoVideoId', 'exif.fileSizeInByte as size']) + .where('assets.deletedAt', 'is', null); + +@Injectable() +export class DownloadRepository { + constructor(@InjectKysely() private db: Kysely) {} + + downloadAssetIds(ids: string[]) { + return builder(this.db).where('assets.id', '=', anyUuid(ids)).stream(); + } + + downloadMotionAssetIds(ids: string[]) { + return builder(this.db).select(['assets.originalPath']).where('assets.id', '=', anyUuid(ids)).stream(); + } + + downloadAlbumId(albumId: string) { + return builder(this.db) + .innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId') + .where('albums_assets_assets.albumsId', '=', albumId) + .stream(); + } + + downloadUserId(userId: string) { + return builder(this.db).where('assets.ownerId', '=', userId).where('assets.isVisible', '=', true).stream(); + } +} diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 180d8ccd4f..8c262edcde 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -9,6 +9,7 @@ import { ConfigRepository } from 'src/repositories/config.repository'; import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; +import { DownloadRepository } from 'src/repositories/download.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; @@ -51,6 +52,7 @@ export const repositories = [ CronRepository, CryptoRepository, DatabaseRepository, + DownloadRepository, EventRepository, JobRepository, LibraryRepository, diff --git a/server/src/repositories/logging.repository.spec.ts b/server/src/repositories/logging.repository.spec.ts index 10c1a6516c..393eeb9496 100644 --- a/server/src/repositories/logging.repository.spec.ts +++ b/server/src/repositories/logging.repository.spec.ts @@ -1,8 +1,8 @@ import { ClsService } from 'nestjs-cls'; import { ImmichWorker } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; -import { LoggingRepository } from 'src/repositories/logging.repository'; -import { mockEnvData, newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; +import { LoggingRepository, MyConsoleLogger } from 'src/repositories/logging.repository'; +import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { Mocked } from 'vitest'; describe(LoggingRepository.name, () => { @@ -18,23 +18,25 @@ describe(LoggingRepository.name, () => { } as unknown as Mocked; }); - describe('formatContext', () => { - it('should use colors', () => { - configMock.getEnv.mockReturnValue(mockEnvData({ noColor: false })); + describe(MyConsoleLogger.name, () => { + describe('formatContext', () => { + it('should use colors', () => { + sut = new LoggingRepository(clsMock, configMock); + sut.setAppName(ImmichWorker.API); - sut = new LoggingRepository(clsMock, configMock); - sut.setAppName(ImmichWorker.API); + const logger = new MyConsoleLogger(clsMock, { color: true }); - expect(sut['formatContext']('context')).toBe('\u001B[33m[Api:context]\u001B[39m '); - }); + expect(logger.formatContext('context')).toBe('\u001B[33m[Api:context]\u001B[39m '); + }); - it('should not use colors when noColor is true', () => { - configMock.getEnv.mockReturnValue(mockEnvData({ noColor: true })); + it('should not use colors when color is false', () => { + sut = new LoggingRepository(clsMock, configMock); + sut.setAppName(ImmichWorker.API); - sut = new LoggingRepository(clsMock, configMock); - sut.setAppName(ImmichWorker.API); + const logger = new MyConsoleLogger(clsMock, { color: false }); - expect(sut['formatContext']('context')).toBe('[Api:context] '); + expect(logger.formatContext('context')).toBe('[Api:context] '); + }); }); }); }); diff --git a/server/src/repositories/logging.repository.ts b/server/src/repositories/logging.repository.ts index 7ddae26a9d..aaf21a3d7c 100644 --- a/server/src/repositories/logging.repository.ts +++ b/server/src/repositories/logging.repository.ts @@ -5,6 +5,9 @@ import { Telemetry } from 'src/decorators'; import { LogLevel } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; +type LogDetails = any[]; +type LogFunction = () => string; + const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; enum LogColor { @@ -16,38 +19,26 @@ enum LogColor { CYAN_BRIGHT = 96, } -@Injectable({ scope: Scope.TRANSIENT }) -@Telemetry({ enabled: false }) -export class LoggingRepository extends ConsoleLogger { - private static logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; - private noColor: boolean; +let appName: string | undefined; +let logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; + +export class MyConsoleLogger extends ConsoleLogger { + private isColorEnabled: boolean; constructor( private cls: ClsService, - configRepository: ConfigRepository, + options?: { color?: boolean; context?: string }, ) { - super(LoggingRepository.name); - - const { noColor } = configRepository.getEnv(); - this.noColor = noColor; - } - - private static appName?: string = undefined; - - setAppName(name: string): void { - LoggingRepository.appName = name.charAt(0).toUpperCase() + name.slice(1); + super(options?.context || MyConsoleLogger.name); + this.isColorEnabled = options?.color || false; } isLevelEnabled(level: LogLevel) { - return isLogLevelEnabled(level, LoggingRepository.logLevels); + return isLogLevelEnabled(level, logLevels); } - setLogLevel(level: LogLevel | false): void { - LoggingRepository.logLevels = level ? LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)) : []; - } - - protected formatContext(context: string): string { - let prefix = LoggingRepository.appName || ''; + formatContext(context: string): string { + let prefix = appName || ''; if (context) { prefix += (prefix ? ':' : '') + context; } @@ -74,6 +65,105 @@ export class LoggingRepository extends ConsoleLogger { }; private withColor(text: string, color: LogColor) { - return this.noColor ? text : `\u001B[${color}m${text}\u001B[39m`; + return this.isColorEnabled ? `\u001B[${color}m${text}\u001B[39m` : text; + } +} + +@Injectable({ scope: Scope.TRANSIENT }) +@Telemetry({ enabled: false }) +export class LoggingRepository { + private logger: MyConsoleLogger; + + constructor(cls: ClsService, configRepository: ConfigRepository) { + const { noColor } = configRepository.getEnv(); + this.logger = new MyConsoleLogger(cls, { context: LoggingRepository.name, color: !noColor }); + } + + setAppName(name: string): void { + appName = name.charAt(0).toUpperCase() + name.slice(1); + } + + setContext(context: string) { + this.logger.setContext(context); + } + + isLevelEnabled(level: LogLevel) { + return this.logger.isLevelEnabled(level); + } + + setLogLevel(level: LogLevel | false): void { + logLevels = level ? LOG_LEVELS.slice(LOG_LEVELS.indexOf(level)) : []; + } + + verbose(message: string, ...details: LogDetails) { + this.handleMessage(LogLevel.VERBOSE, message, details); + } + + verboseFn(message: LogFunction, ...details: LogDetails) { + this.handleFunction(LogLevel.VERBOSE, message, details); + } + + debug(message: string, ...details: LogDetails) { + this.handleMessage(LogLevel.DEBUG, message, details); + } + + debugFn(message: LogFunction, ...details: LogDetails) { + this.handleFunction(LogLevel.DEBUG, message, details); + } + + log(message: string, ...details: LogDetails) { + this.handleMessage(LogLevel.LOG, message, details); + } + + warn(message: string, ...details: LogDetails) { + this.handleMessage(LogLevel.WARN, message, details); + } + + error(message: string | Error, ...details: LogDetails) { + this.handleMessage(LogLevel.ERROR, message, details); + } + + fatal(message: string, ...details: LogDetails) { + this.handleMessage(LogLevel.FATAL, message, details); + } + + private handleFunction(level: LogLevel, message: LogFunction, details: LogDetails[]) { + if (this.logger.isLevelEnabled(level)) { + this.handleMessage(level, message(), details); + } + } + + private handleMessage(level: LogLevel, message: string | Error, details: LogDetails[]) { + switch (level) { + case LogLevel.VERBOSE: { + this.logger.verbose(message, ...details); + break; + } + + case LogLevel.DEBUG: { + this.logger.debug(message, ...details); + break; + } + + case LogLevel.LOG: { + this.logger.log(message, ...details); + break; + } + + case LogLevel.WARN: { + this.logger.warn(message, ...details); + break; + } + + case LogLevel.ERROR: { + this.logger.error(message, ...details); + break; + } + + case LogLevel.FATAL: { + this.logger.fatal(message, ...details); + break; + } + } } } diff --git a/server/src/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts index 8145bf3154..5e916c71f3 100644 --- a/server/src/repositories/machine-learning.repository.ts +++ b/server/src/repositories/machine-learning.repository.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { readFile } from 'node:fs/promises'; +import { MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME, MACHINE_LEARNING_PING_TIMEOUT } from 'src/constants'; import { CLIPConfig } from 'src/dtos/model-config.dto'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -55,16 +56,80 @@ export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | Fa @Injectable() export class MachineLearningRepository { + // Note that deleted URL's are not removed from this map (ie: they're leaked) + // Cleaning them up is low priority since there should be very few over a + // typical server uptime cycle + private urlAvailability: { + [url: string]: + | { + active: boolean; + lastChecked: number; + } + | undefined; + }; + constructor(private logger: LoggingRepository) { this.logger.setContext(MachineLearningRepository.name); + this.urlAvailability = {}; + } + + private setUrlAvailability(url: string, active: boolean) { + const current = this.urlAvailability[url]; + if (current?.active !== active) { + this.logger.verbose(`Setting ${url} ML server to ${active ? 'active' : 'inactive'}.`); + } + this.urlAvailability[url] = { + active, + lastChecked: Date.now(), + }; + } + + private async checkAvailability(url: string) { + let active = false; + try { + const response = await fetch(new URL('/ping', url), { + signal: AbortSignal.timeout(MACHINE_LEARNING_PING_TIMEOUT), + }); + active = response.ok; + } catch {} + this.setUrlAvailability(url, active); + return active; + } + + private async shouldSkipUrl(url: string) { + const availability = this.urlAvailability[url]; + if (availability === undefined) { + // If this is a new endpoint, then check inline and skip if it fails + if (!(await this.checkAvailability(url))) { + return true; + } + return false; + } + if (!availability.active && Date.now() - availability.lastChecked < MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME) { + // If this is an old inactive endpoint that hasn't been checked in a + // while then check but don't wait for the result, just skip it + // This avoids delays on every search whilst allowing higher priority + // ML servers to recover over time. + void this.checkAvailability(url); + return true; + } + return false; } private async predict(urls: string[], payload: ModelPayload, config: MachineLearningRequest): Promise { const formData = await this.getFormData(payload, config); + let urlCounter = 0; for (const url of urls) { + urlCounter++; + const isLast = urlCounter >= urls.length; + if (!isLast && (await this.shouldSkipUrl(url))) { + continue; + } + try { const response = await fetch(new URL('/predict', url), { method: 'POST', body: formData }); if (response.ok) { + this.setUrlAvailability(url, true); return response.json(); } @@ -76,6 +141,7 @@ export class MachineLearningRepository { `Machine learning request to "${url}" failed: ${error instanceof Error ? error.message : error}`, ); } + this.setUrlAvailability(url, false); } throw new Error(`Machine learning request '${JSON.stringify(config)}' failed for all URLs`); diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 965e7ffd13..442225f7c8 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -10,7 +10,7 @@ import { citiesFile } from 'src/constants'; import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity'; -import { LogLevel, SystemMetadataKey } from 'src/enum'; +import { SystemMetadataKey } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; @@ -137,9 +137,7 @@ export class MapRepository { .executeTakeFirst(); if (response) { - if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) { - this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`); - } + this.logger.verboseFn(() => `Raw: ${JSON.stringify(response, null, 2)}`); const { countryCode, name: city, admin1Name } = response; const country = getName(countryCode, 'en') ?? null; @@ -167,9 +165,8 @@ export class MapRepository { return { country: null, state: null, city: null }; } - if (this.logger.isLevelEnabled(LogLevel.VERBOSE)) { - this.logger.verbose(`Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`); - } + this.logger.verboseFn(() => `Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`); + const { admin_a3 } = ne_response; const country = getName(admin_a3, 'en') ?? null; const state = null; diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index 3f297d709b..5df37a5ea7 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -85,6 +85,10 @@ export class MetadataRepository { this.logger.setContext(MetadataRepository.name); } + setMaxConcurrency(concurrency: number) { + this.exiftool.batchCluster.setMaxProcs(concurrency); + } + async teardown() { await this.exiftool.end(); } diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts index c46259fa9b..706e23cef7 100644 --- a/server/src/repositories/move.repository.ts +++ b/server/src/repositories/move.repository.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { Insertable, Kysely, Updateable } from 'kysely'; +import { Insertable, Kysely, sql, Updateable } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; import { DB, MoveHistory } from 'src/db'; import { DummyValue, GenerateSql } from 'src/decorators'; import { MoveEntity } from 'src/entities/move.entity'; -import { PathType } from 'src/enum'; +import { AssetPathType, PathType } from 'src/enum'; export type MoveCreate = Pick & Partial; @@ -47,4 +47,28 @@ export class MoveRepository { .returningAll() .executeTakeFirstOrThrow() as unknown as Promise; } + + @GenerateSql() + async cleanMoveHistory(): Promise { + await this.db + .deleteFrom('move_history') + .where((eb) => + eb( + 'move_history.entityId', + 'not in', + eb.selectFrom('assets').select('id').whereRef('assets.id', '=', 'move_history.entityId'), + ), + ) + .where('move_history.pathType', '=', sql.lit(AssetPathType.ORIGINAL)) + .execute(); + } + + @GenerateSql() + async cleanMoveHistorySingle(assetId: string): Promise { + await this.db + .deleteFrom('move_history') + .where('move_history.pathType', '=', sql.lit(AssetPathType.ORIGINAL)) + .where('entityId', '=', assetId) + .execute(); + } } diff --git a/server/src/repositories/notification.repository.spec.ts b/server/src/repositories/notification.repository.spec.ts index 7707069dd9..6253697087 100644 --- a/server/src/repositories/notification.repository.spec.ts +++ b/server/src/repositories/notification.repository.spec.ts @@ -1,16 +1,11 @@ -import { LoggingRepository } from 'src/repositories/logging.repository'; import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository'; -import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { Mocked } from 'vitest'; +import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock'; describe(NotificationRepository.name, () => { let sut: NotificationRepository; - let loggerMock: Mocked; beforeEach(() => { - loggerMock = newLoggingRepositoryMock() as ILoggingRepository as Mocked; - - sut = new NotificationRepository(loggerMock as LoggingRepository); + sut = new NotificationRepository(newFakeLoggingRepository()); }); describe('renderEmail', () => { diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 2f313aa083..46f38db55f 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -318,6 +318,7 @@ export class SearchRepository { .where('assets.isVisible', '=', true) .where('assets.type', '=', type) .where('assets.id', '!=', asUuid(assetId)) + .where('assets.stackId', 'is', null) .orderBy(sql`smart_search.embedding <=> ${embedding}`) .limit(64), ) diff --git a/server/src/repositories/storage.repository.spec.ts b/server/src/repositories/storage.repository.spec.ts index 3ab9e615ec..93b21a7f9b 100644 --- a/server/src/repositories/storage.repository.spec.ts +++ b/server/src/repositories/storage.repository.spec.ts @@ -1,9 +1,7 @@ import mockfs from 'mock-fs'; import { CrawlOptionsDto } from 'src/dtos/library.dto'; -import { LoggingRepository } from 'src/repositories/logging.repository'; import { StorageRepository } from 'src/repositories/storage.repository'; -import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; -import { Mocked } from 'vitest'; +import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock'; interface Test { test: string; @@ -182,11 +180,9 @@ const tests: Test[] = [ describe(StorageRepository.name, () => { let sut: StorageRepository; - let logger: Mocked; beforeEach(() => { - logger = newLoggingRepositoryMock(); - sut = new StorageRepository(logger as ILoggingRepository as LoggingRepository); + sut = new StorageRepository(newFakeLoggingRepository()); }); afterEach(() => { diff --git a/server/src/repositories/sync.repository.ts b/server/src/repositories/sync.repository.ts index 4023bf890e..f2c5a1fc16 100644 --- a/server/src/repositories/sync.repository.ts +++ b/server/src/repositories/sync.repository.ts @@ -1,7 +1,6 @@ import { Injectable } from '@nestjs/common'; import { Insertable, Kysely, sql } from 'kysely'; import { InjectKysely } from 'nestjs-kysely'; -import { columns } from 'src/database'; import { DB, SessionSyncCheckpoints } from 'src/db'; import { SyncEntityType } from 'src/enum'; import { SyncAck } from 'src/types'; @@ -41,40 +40,42 @@ export class SyncRepository { getUserUpserts(ack?: SyncAck) { return this.db .selectFrom('users') - .select(['id', 'name', 'email', 'deletedAt']) - .select(columns.ackEpoch('updatedAt')) - .$if(!!ack, (qb) => - qb.where((eb) => - eb.or([ - eb(eb.fn('to_timestamp', [sql.val(ack!.ackEpoch)]), '<', eb.ref('updatedAt')), - eb.and([ - eb(eb.fn('to_timestamp', [sql.val(ack!.ackEpoch)]), '<=', eb.ref('updatedAt')), - eb('id', '>', ack!.ids[0]), - ]), - ]), - ), - ) - .orderBy(['updatedAt asc', 'id asc']) + .select(['id', 'name', 'email', 'deletedAt', 'updateId']) + .$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId)) + .where('updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .orderBy(['updateId asc']) .stream(); } getUserDeletes(ack?: SyncAck) { return this.db .selectFrom('users_audit') - .select(['userId']) - .select(columns.ackEpoch('deletedAt')) - .$if(!!ack, (qb) => - qb.where((eb) => - eb.or([ - eb(eb.fn('to_timestamp', [sql.val(ack!.ackEpoch)]), '<', eb.ref('deletedAt')), - eb.and([ - eb(eb.fn('to_timestamp', [sql.val(ack!.ackEpoch)]), '<=', eb.ref('deletedAt')), - eb('userId', '>', ack!.ids[0]), - ]), - ]), - ), - ) - .orderBy(['deletedAt asc', 'userId asc']) + .select(['id', 'userId']) + .$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId)) + .where('deletedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .orderBy(['id asc']) + .stream(); + } + + getPartnerUpserts(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('partners') + .select(['sharedById', 'sharedWithId', 'inTimeline', 'updateId']) + .$if(!!ack, (qb) => qb.where('updateId', '>', ack!.updateId)) + .where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)])) + .where('updatedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .orderBy(['updateId asc']) + .stream(); + } + + getPartnerDeletes(userId: string, ack?: SyncAck) { + return this.db + .selectFrom('partners_audit') + .select(['id', 'sharedById', 'sharedWithId']) + .$if(!!ack, (qb) => qb.where('id', '>', ack!.updateId)) + .where((eb) => eb.or([eb('sharedById', '=', userId), eb('sharedWithId', '=', userId)])) + .where('deletedAt', '<', sql.raw("now() - interval '1 millisecond'")) + .orderBy(['id asc']) .stream(); } } diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts index c5156e1837..c0ca6ebf37 100644 --- a/server/src/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -1,209 +1,188 @@ import { Injectable } from '@nestjs/common'; -import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Insertable, Kysely, sql, Updateable } from 'kysely'; +import { InjectKysely } from 'nestjs-kysely'; +import { columns } from 'src/database'; +import { DB, TagAsset, Tags } from 'src/db'; import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; -import { TagEntity } from 'src/entities/tag.entity'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { DataSource, In, Repository } from 'typeorm'; - -export type AssetTagItem = { assetId: string; tagId: string }; @Injectable() export class TagRepository { constructor( - @InjectDataSource() private dataSource: DataSource, - @InjectRepository(TagEntity) private repository: Repository, + @InjectKysely() private db: Kysely, private logger: LoggingRepository, ) { this.logger.setContext(TagRepository.name); } - get(id: string): Promise { - return this.repository.findOne({ where: { id } }); + @GenerateSql({ params: [DummyValue.UUID] }) + get(id: string) { + return this.db.selectFrom('tags').select(columns.tagDto).where('id', '=', id).executeTakeFirst(); } - getByValue(userId: string, value: string): Promise { - return this.repository.findOne({ where: { userId, value } }); + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + getByValue(userId: string, value: string) { + return this.db + .selectFrom('tags') + .select(columns.tagDto) + .where('userId', '=', userId) + .where('value', '=', value) + .executeTakeFirst(); } - async upsertValue({ - userId, - value, - parent, - }: { - userId: string; - value: string; - parent?: TagEntity; - }): Promise { - return this.dataSource.transaction(async (manager) => { - // upsert tag - const { identifiers } = await manager.upsert( - TagEntity, - { userId, value, parentId: parent?.id }, - { conflictPaths: { userId: true, value: true } }, - ); - const id = identifiers[0]?.id; - if (!id) { - throw new Error('Failed to upsert tag'); - } + @GenerateSql({ params: [{ userId: DummyValue.UUID, value: DummyValue.STRING, parentId: DummyValue.UUID }] }) + async upsertValue({ userId, value, parentId: _parentId }: { userId: string; value: string; parentId?: string }) { + const parentId = _parentId ?? null; + return this.db.transaction().execute(async (tx) => { + const tag = await this.db + .insertInto('tags') + .values({ userId, value, parentId }) + .onConflict((oc) => oc.columns(['userId', 'value']).doUpdateSet({ parentId })) + .returningAll() + .executeTakeFirstOrThrow(); // update closure table - await manager.query( - `INSERT INTO tags_closure (id_ancestor, id_descendant) - VALUES ($1, $1) - ON CONFLICT DO NOTHING;`, - [id], - ); + await tx + .insertInto('tags_closure') + .values({ id_ancestor: tag.id, id_descendant: tag.id }) + .onConflict((oc) => oc.doNothing()) + .execute(); - if (parent) { - await manager.query( - `INSERT INTO tags_closure (id_ancestor, id_descendant) - SELECT id_ancestor, '${id}' as id_descendant FROM tags_closure WHERE id_descendant = $1 - ON CONFLICT DO NOTHING`, - [parent.id], - ); + if (parentId) { + await tx + .insertInto('tags_closure') + .columns(['id_ancestor', 'id_descendant']) + .expression( + this.db + .selectFrom('tags_closure') + .select(['id_ancestor', sql.raw(`'${tag.id}'`).as('id_descendant')]) + .where('id_descendant', '=', parentId), + ) + .onConflict((oc) => oc.doNothing()) + .execute(); } - return manager.findOneOrFail(TagEntity, { where: { id } }); + return tag; }); } - async getAll(userId: string): Promise { - const tags = await this.repository.find({ - where: { userId }, - order: { - value: 'ASC', - }, - }); - - return tags; + @GenerateSql({ params: [DummyValue.UUID] }) + getAll(userId: string) { + return this.db + .selectFrom('tags') + .select(columns.tagDto) + .where('userId', '=', userId) + .orderBy('value asc') + .execute(); } - create(tag: Partial): Promise { - return this.save(tag); + @GenerateSql({ params: [{ userId: DummyValue.UUID, color: DummyValue.STRING, value: DummyValue.STRING }] }) + create(tag: Insertable) { + return this.db.insertInto('tags').values(tag).returningAll().executeTakeFirstOrThrow(); } - update(tag: Partial): Promise { - return this.save(tag); + @GenerateSql({ params: [DummyValue.UUID, { color: DummyValue.STRING }] }) + update(id: string, dto: Updateable) { + return this.db.updateTable('tags').set(dto).where('id', '=', id).returningAll().executeTakeFirstOrThrow(); } - async delete(id: string): Promise { - await this.repository.delete(id); + @GenerateSql({ params: [DummyValue.UUID] }) + async delete(id: string) { + await this.db.deleteFrom('tags').where('id', '=', id).execute(); } - @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @ChunkedSet({ paramIndex: 1 }) + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) async getAssetIds(tagId: string, assetIds: string[]): Promise> { if (assetIds.length === 0) { return new Set(); } - const results = await this.dataSource - .createQueryBuilder() - .select('tag_asset.assetsId', 'assetId') - .from('tag_asset', 'tag_asset') - .where('"tag_asset"."tagsId" = :tagId', { tagId }) - .andWhere('"tag_asset"."assetsId" IN (:...assetIds)', { assetIds }) - .getRawMany<{ assetId: string }>(); + const results = await this.db + .selectFrom('tag_asset') + .select(['assetsId as assetId']) + .where('tagsId', '=', tagId) + .where('assetsId', 'in', assetIds) + .execute(); return new Set(results.map(({ assetId }) => assetId)); } + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + @Chunked({ paramIndex: 1 }) async addAssetIds(tagId: string, assetIds: string[]): Promise { if (assetIds.length === 0) { return; } - await this.dataSource.manager - .createQueryBuilder() - .insert() - .into('tag_asset', ['tagsId', 'assetsId']) + await this.db + .insertInto('tag_asset') .values(assetIds.map((assetId) => ({ tagsId: tagId, assetsId: assetId }))) .execute(); } + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) @Chunked({ paramIndex: 1 }) async removeAssetIds(tagId: string, assetIds: string[]): Promise { if (assetIds.length === 0) { return; } - await this.dataSource - .createQueryBuilder() - .delete() - .from('tag_asset') - .where({ - tagsId: tagId, - assetsId: In(assetIds), - }) - .execute(); + await this.db.deleteFrom('tag_asset').where('tagsId', '=', tagId).where('assetsId', 'in', assetIds).execute(); } + @GenerateSql({ params: [{ assetId: DummyValue.UUID, tagsIds: [DummyValue.UUID] }] }) @Chunked() - async upsertAssetIds(items: AssetTagItem[]): Promise { + upsertAssetIds(items: Insertable[]) { if (items.length === 0) { - return []; + return Promise.resolve([]); } - const { identifiers } = await this.dataSource - .createQueryBuilder() - .insert() - .into('tag_asset', ['assetsId', 'tagsId']) - .values(items.map(({ assetId, tagId }) => ({ assetsId: assetId, tagsId: tagId }))) + return this.db + .insertInto('tag_asset') + .values(items) + .onConflict((oc) => oc.doNothing()) + .returningAll() .execute(); - - return (identifiers as Array<{ assetsId: string; tagsId: string }>).map(({ assetsId, tagsId }) => ({ - assetId: assetsId, - tagId: tagsId, - })); } - async upsertAssetTags({ assetId, tagIds }: { assetId: string; tagIds: string[] }) { - await this.dataSource.transaction(async (manager) => { - await manager.createQueryBuilder().delete().from('tag_asset').where({ assetsId: assetId }).execute(); + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + @Chunked({ paramIndex: 1 }) + replaceAssetTags(assetId: string, tagIds: string[]) { + return this.db.transaction().execute(async (tx) => { + await tx.deleteFrom('tag_asset').where('assetsId', '=', assetId).execute(); if (tagIds.length === 0) { return; } - await manager - .createQueryBuilder() - .insert() - .into('tag_asset', ['tagsId', 'assetsId']) + return tx + .insertInto('tag_asset') .values(tagIds.map((tagId) => ({ tagsId: tagId, assetsId: assetId }))) + .onConflict((oc) => oc.doNothing()) + .returningAll() .execute(); }); } + @GenerateSql() async deleteEmptyTags() { - await this.dataSource.transaction(async (manager) => { - const ids = new Set(); - const tags = await manager.find(TagEntity); - for (const tag of tags) { - const count = await manager - .createQueryBuilder('assets', 'asset') - .innerJoin( - 'asset.tags', - 'asset_tags', - 'asset_tags.id IN (SELECT id_descendant FROM tags_closure WHERE id_ancestor = :tagId)', - { tagId: tag.id }, - ) - .getCount(); + // TODO rewrite as a single statement + await this.db.transaction().execute(async (tx) => { + const result = await tx + .selectFrom('assets') + .innerJoin('tag_asset', 'tag_asset.assetsId', 'assets.id') + .innerJoin('tags_closure', 'tags_closure.id_descendant', 'tag_asset.tagsId') + .innerJoin('tags', 'tags.id', 'tags_closure.id_descendant') + .select((eb) => ['tags.id', eb.fn.count('assets.id').as('count')]) + .groupBy('tags.id') + .execute(); - if (count === 0) { - this.logger.debug(`Found empty tag: ${tag.id} - ${tag.value}`); - ids.add(tag.id); - } - } - - if (ids.size > 0) { - await manager.delete(TagEntity, { id: In([...ids]) }); - this.logger.log(`Deleted ${ids.size} empty tags`); + const ids = result.filter(({ count }) => count === 0).map(({ id }) => id); + if (ids.length > 0) { + await this.db.deleteFrom('tags').where('id', 'in', ids).execute(); + this.logger.log(`Deleted ${ids.length} empty tags`); } }); } - - private async save(partial: Partial): Promise { - const { id } = await this.repository.save(partial); - return this.repository.findOneOrFail({ where: { id } }); - } } diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 7d9a4f3776..44c3015330 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -15,7 +15,7 @@ export class APIKeyService extends BaseService { throw new BadRequestException('Cannot grant permissions you do not have'); } - const entity = await this.keyRepository.create({ + const entity = await this.apiKeyRepository.create({ key: this.cryptoRepository.hashSha256(secret), name: dto.name || 'API Key', userId: auth.user.id, @@ -26,27 +26,27 @@ export class APIKeyService extends BaseService { } async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise { - const exists = await this.keyRepository.getById(auth.user.id, id); + const exists = await this.apiKeyRepository.getById(auth.user.id, id); if (!exists) { throw new BadRequestException('API Key not found'); } - const key = await this.keyRepository.update(auth.user.id, id, { name: dto.name }); + const key = await this.apiKeyRepository.update(auth.user.id, id, { name: dto.name }); return this.map(key); } async delete(auth: AuthDto, id: string): Promise { - const exists = await this.keyRepository.getById(auth.user.id, id); + const exists = await this.apiKeyRepository.getById(auth.user.id, id); if (!exists) { throw new BadRequestException('API Key not found'); } - await this.keyRepository.delete(auth.user.id, id); + await this.apiKeyRepository.delete(auth.user.id, id); } async getById(auth: AuthDto, id: string): Promise { - const key = await this.keyRepository.getById(auth.user.id, id); + const key = await this.apiKeyRepository.getById(auth.user.id, id); if (!key) { throw new BadRequestException('API Key not found'); } @@ -54,7 +54,7 @@ export class APIKeyService extends BaseService { } async getAll(auth: AuthDto): Promise { - const keys = await this.keyRepository.getByUserId(auth.user.id); + const keys = await this.apiKeyRepository.getByUserId(auth.user.id); return keys.map((key) => this.map(key)); } diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 336c3ac8f0..f91f600bb1 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -64,18 +64,18 @@ describe(AssetService.name, () => { mocks.partner.getAll.mockResolvedValue([]); mocks.asset.getByDayOfYear.mockResolvedValue([ { - yearsAgo: 1, + year: 2023, assets: [image1, image2], }, { - yearsAgo: 9, + year: 2015, assets: [image3], }, { - yearsAgo: 15, + year: 2009, assets: [image4], }, - ]); + ] as any); await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([ { yearsAgo: 1, title: '1 year ago', assets: [mapAsset(image1), mapAsset(image2)] }, diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index a9b723c9f9..df66d405b7 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -38,12 +38,15 @@ export class AssetService extends BaseService { const userIds = [auth.user.id, ...partnerIds]; const groups = await this.assetRepository.getByDayOfYear(userIds, dto); - return groups.map(({ yearsAgo, assets }) => ({ - yearsAgo, - // TODO move this to clients - title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} ago`, - assets: assets.map((asset) => mapAsset(asset, { auth })), - })); + return groups.map(({ year, assets }) => { + const yearsAgo = DateTime.utc().year - year; + return { + yearsAgo, + // TODO move this to clients + title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} ago`, + assets: assets.map((asset) => mapAsset(asset as AssetEntity, { auth })), + }; + }); } async getStatistics(auth: AuthDto, dto: AssetStatsDto) { diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 35d48cf57e..4ed3db8419 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -307,7 +307,7 @@ export class AuthService extends BaseService { private async validateApiKey(key: string): Promise { const hashedKey = this.cryptoRepository.hashSha256(key); - const apiKey = await this.keyRepository.getKey(hashedKey); + const apiKey = await this.apiKeyRepository.getKey(hashedKey); if (apiKey?.user) { return { user: apiKey.user, diff --git a/server/src/services/base.service.ts b/server/src/services/base.service.ts index 63cca43cc2..f8c995c007 100644 --- a/server/src/services/base.service.ts +++ b/server/src/services/base.service.ts @@ -17,6 +17,7 @@ import { ConfigRepository } from 'src/repositories/config.repository'; import { CronRepository } from 'src/repositories/cron.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; +import { DownloadRepository } from 'src/repositories/download.repository'; import { EventRepository } from 'src/repositories/event.repository'; import { JobRepository } from 'src/repositories/job.repository'; import { LibraryRepository } from 'src/repositories/library.repository'; @@ -57,17 +58,18 @@ export class BaseService { protected logger: LoggingRepository, protected accessRepository: AccessRepository, protected activityRepository: ActivityRepository, - protected auditRepository: AuditRepository, protected albumRepository: AlbumRepository, protected albumUserRepository: AlbumUserRepository, + protected apiKeyRepository: ApiKeyRepository, protected assetRepository: AssetRepository, + protected auditRepository: AuditRepository, protected configRepository: ConfigRepository, protected cronRepository: CronRepository, protected cryptoRepository: CryptoRepository, protected databaseRepository: DatabaseRepository, + protected downloadRepository: DownloadRepository, protected eventRepository: EventRepository, protected jobRepository: JobRepository, - protected keyRepository: ApiKeyRepository, protected libraryRepository: LibraryRepository, protected machineLearningRepository: MachineLearningRepository, protected mapRepository: MapRepository, diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index d9e60dfdb4..5139fbd58f 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -1,10 +1,9 @@ import { BadRequestException } from '@nestjs/common'; import { DownloadResponseDto } from 'src/dtos/download.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { DownloadService } from 'src/services/download.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { newTestService, ServiceMocks } from 'test/utils'; +import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { Readable } from 'typeorm/platform/PlatformTools.js'; import { vitest } from 'vitest'; @@ -12,7 +11,7 @@ const downloadResponse: DownloadResponseDto = { totalSize: 105_000, archives: [ { - assetIds: ['asset-id', 'asset-id'], + assetIds: ['asset-1', 'asset-2'], size: 105_000, }, ], @@ -172,53 +171,60 @@ describe(DownloadService.name, () => { }); it('should return a list of archives (assetIds)', async () => { - mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - mocks.asset.getByIds.mockResolvedValue([assetStub.image, assetStub.video]); - const assetIds = ['asset-1', 'asset-2']; + + mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds)); + mocks.downloadRepository.downloadAssetIds.mockReturnValue( + makeStream([ + { id: 'asset-1', livePhotoVideoId: null, size: 100_000 }, + { id: 'asset-2', livePhotoVideoId: null, size: 5000 }, + ]), + ); + await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual(downloadResponse); - expect(mocks.asset.getByIds).toHaveBeenCalledWith(['asset-1', 'asset-2'], { exifInfo: true }); + expect(mocks.downloadRepository.downloadAssetIds).toHaveBeenCalledWith(['asset-1', 'asset-2']); }); it('should return a list of archives (albumId)', async () => { mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1'])); - mocks.asset.getByAlbumId.mockResolvedValue({ - items: [assetStub.image, assetStub.video], - hasNextPage: false, - }); + mocks.downloadRepository.downloadAlbumId.mockReturnValue( + makeStream([ + { id: 'asset-1', livePhotoVideoId: null, size: 100_000 }, + { id: 'asset-2', livePhotoVideoId: null, size: 5000 }, + ]), + ); await expect(sut.getDownloadInfo(authStub.admin, { albumId: 'album-1' })).resolves.toEqual(downloadResponse); expect(mocks.access.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['album-1'])); - expect(mocks.asset.getByAlbumId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, 'album-1'); + expect(mocks.downloadRepository.downloadAlbumId).toHaveBeenCalledWith('album-1'); }); it('should return a list of archives (userId)', async () => { - mocks.asset.getByUserId.mockResolvedValue({ - items: [assetStub.image, assetStub.video], - hasNextPage: false, - }); + mocks.downloadRepository.downloadUserId.mockReturnValue( + makeStream([ + { id: 'asset-1', livePhotoVideoId: null, size: 100_000 }, + { id: 'asset-2', livePhotoVideoId: null, size: 5000 }, + ]), + ); await expect(sut.getDownloadInfo(authStub.admin, { userId: authStub.admin.user.id })).resolves.toEqual( downloadResponse, ); - expect(mocks.asset.getByUserId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, authStub.admin.user.id, { - isVisible: true, - }); + expect(mocks.downloadRepository.downloadUserId).toHaveBeenCalledWith(authStub.admin.user.id); }); it('should split archives by size', async () => { - mocks.asset.getByUserId.mockResolvedValue({ - items: [ - { ...assetStub.image, id: 'asset-1' }, - { ...assetStub.video, id: 'asset-2' }, - { ...assetStub.withLocation, id: 'asset-3' }, - { ...assetStub.noWebpPath, id: 'asset-4' }, - ], - hasNextPage: false, - }); + mocks.downloadRepository.downloadUserId.mockReturnValue( + makeStream([ + { id: 'asset-1', livePhotoVideoId: null, size: 5000 }, + { id: 'asset-2', livePhotoVideoId: null, size: 100_000 }, + { id: 'asset-3', livePhotoVideoId: null, size: 23_456 }, + { id: 'asset-4', livePhotoVideoId: null, size: 123_000 }, + ]), + ); await expect( sut.getDownloadInfo(authStub.admin, { @@ -235,49 +241,53 @@ describe(DownloadService.name, () => { }); it('should include the video portion of a live photo', async () => { - const assetIds = [assetStub.livePhotoStillAsset.id]; - const assets = [assetStub.livePhotoStillAsset, assetStub.livePhotoMotionAsset]; + const assetIds = ['asset-1', 'asset-2']; mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds)); - mocks.asset.getByIds.mockImplementation( - (ids) => - Promise.resolve( - ids.map((id) => assets.find((asset) => asset.id === id)).filter((asset) => !!asset), - ) as Promise, + mocks.downloadRepository.downloadAssetIds.mockReturnValue( + makeStream([ + { id: 'asset-1', livePhotoVideoId: 'asset-3', size: 5000 }, + { id: 'asset-2', livePhotoVideoId: 'asset-4', size: 100_000 }, + ]), ); - await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual({ - totalSize: 125_000, + mocks.downloadRepository.downloadMotionAssetIds.mockReturnValue( + makeStream([ + { id: 'asset-3', livePhotoVideoId: null, size: 23_456, originalPath: '/path/to/file.mp4' }, + { id: 'asset-4', livePhotoVideoId: null, size: 123_000, originalPath: '/path/to/file.mp4' }, + ]), + ); + + await expect(sut.getDownloadInfo(authStub.admin, { assetIds, archiveSize: 30_000 })).resolves.toEqual({ + totalSize: 251_456, archives: [ - { - assetIds: [assetStub.livePhotoStillAsset.id, assetStub.livePhotoMotionAsset.id], - size: 125_000, - }, + { assetIds: ['asset-1', 'asset-2'], size: 105_000 }, + { assetIds: ['asset-3', 'asset-4'], size: 146_456 }, ], }); }); it('should skip the video portion of an android live photo by default', async () => { - const assetIds = [assetStub.livePhotoStillAsset.id]; - const assets = [ - assetStub.livePhotoStillAsset, - { ...assetStub.livePhotoMotionAsset, originalPath: 'upload/encoded-video/uuid-MP.mp4' }, - ]; + const assetIds = ['asset-1']; mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds)); - mocks.asset.getByIds.mockImplementation( - (ids) => - Promise.resolve( - ids.map((id) => assets.find((asset) => asset.id === id)).filter((asset) => !!asset), - ) as Promise, + + mocks.downloadRepository.downloadAssetIds.mockReturnValue( + makeStream([{ id: 'asset-1', livePhotoVideoId: 'asset-3', size: 5000 }]), + ); + + mocks.downloadRepository.downloadMotionAssetIds.mockReturnValue( + makeStream([ + { id: 'asset-2', livePhotoVideoId: null, size: 23_456, originalPath: 'upload/encoded-video/uuid-MP.mp4' }, + ]), ); await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual({ - totalSize: 25_000, + totalSize: 5000, archives: [ { - assetIds: [assetStub.livePhotoStillAsset.id], - size: 25_000, + assetIds: ['asset-1'], + size: 5000, }, ], }); diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index 8b18bd0a07..cb664aea32 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -4,53 +4,72 @@ import { StorageCore } from 'src/cores/storage.core'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; import { ImmichReadStream } from 'src/repositories/storage.repository'; import { BaseService } from 'src/services/base.service'; import { HumanReadableSize } from 'src/utils/bytes'; -import { usePagination } from 'src/utils/pagination'; import { getPreferences } from 'src/utils/preferences'; @Injectable() export class DownloadService extends BaseService { async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise { + let assets; + + if (dto.assetIds) { + const assetIds = dto.assetIds; + await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds }); + assets = this.downloadRepository.downloadAssetIds(assetIds); + } else if (dto.albumId) { + const albumId = dto.albumId; + await this.requireAccess({ auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] }); + assets = this.downloadRepository.downloadAlbumId(albumId); + } else if (dto.userId) { + const userId = dto.userId; + await this.requireAccess({ auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] }); + assets = this.downloadRepository.downloadUserId(userId); + } else { + throw new BadRequestException('assetIds, albumId, or userId is required'); + } + const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4; + const metadata = await this.userRepository.getMetadata(auth.user.id); + const preferences = getPreferences(auth.user.email, metadata); + const motionIds = new Set(); const archives: DownloadArchiveInfo[] = []; let archive: DownloadArchiveInfo = { size: 0, assetIds: [] }; - const metadata = await this.userRepository.getMetadata(auth.user.id); - const preferences = getPreferences(auth.user.email, metadata); + const addToArchive = ({ id, size }: { id: string; size: number | null }) => { + archive.assetIds.push(id); + archive.size += Number(size || 0); - const assetPagination = await this.getDownloadAssets(auth, dto); - for await (const assets of assetPagination) { - // motion part of live photos - const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter((id): id is string => !!id); - if (motionIds.length > 0) { - const motionAssets = await this.assetRepository.getByIds(motionIds, { exifInfo: true }); - for (const motionAsset of motionAssets) { - if ( - !StorageCore.isAndroidMotionPath(motionAsset.originalPath) || - preferences.download.includeEmbeddedVideos - ) { - assets.push(motionAsset); - } - } - } - - for (const asset of assets) { - archive.size += Number(asset.exifInfo?.fileSizeInByte || 0); - archive.assetIds.push(asset.id); - - if (archive.size > targetSize) { - archives.push(archive); - archive = { size: 0, assetIds: [] }; - } - } - - if (archive.assetIds.length > 0) { + if (archive.size > targetSize) { archives.push(archive); + archive = { size: 0, assetIds: [] }; } + }; + + for await (const asset of assets) { + // motion part of live photos + if (asset.livePhotoVideoId) { + motionIds.add(asset.livePhotoVideoId); + } + + addToArchive(asset); + } + + if (motionIds.size > 0) { + const motionAssets = this.downloadRepository.downloadMotionAssetIds([...motionIds]); + for await (const motionAsset of motionAssets) { + if (StorageCore.isAndroidMotionPath(motionAsset.originalPath) && !preferences.download.includeEmbeddedVideos) { + continue; + } + + addToArchive(motionAsset); + } + } + + if (archive.assetIds.length > 0) { + archives.push(archive); } let totalSize = 0; @@ -99,31 +118,4 @@ export class DownloadService extends BaseService { return { stream: zip.stream }; } - - private async getDownloadAssets(auth: AuthDto, dto: DownloadInfoDto): Promise> { - const PAGINATION_SIZE = 2500; - - if (dto.assetIds) { - const assetIds = dto.assetIds; - await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds }); - const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true }); - return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets })); - } - - if (dto.albumId) { - const albumId = dto.albumId; - await this.requireAccess({ auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] }); - return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId)); - } - - if (dto.userId) { - const userId = dto.userId; - await this.requireAccess({ auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] }); - return usePagination(PAGINATION_SIZE, (pagination) => - this.assetRepository.getByUserId(pagination, userId, { isVisible: true }), - ); - } - - throw new BadRequestException('assetIds, albumId, or userId is required'); - } } diff --git a/server/src/services/duplicate.service.spec.ts b/server/src/services/duplicate.service.spec.ts index 30b8cd3451..8be943eaf0 100644 --- a/server/src/services/duplicate.service.spec.ts +++ b/server/src/services/duplicate.service.spec.ts @@ -173,6 +173,16 @@ describe(SearchService.name, () => { expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`); }); + it('should skip if asset is part of stack', async () => { + const id = assetStub.primaryImage.id; + mocks.asset.getById.mockResolvedValue(assetStub.primaryImage); + + const result = await sut.handleSearchDuplicates({ id }); + + expect(result).toBe(JobStatus.SKIPPED); + expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is part of a stack, skipping`); + }); + it('should skip if asset is not visible', async () => { const id = assetStub.livePhotoMotionAsset.id; mocks.asset.getById.mockResolvedValue(assetStub.livePhotoMotionAsset); diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 5600033b47..74b86f8e4e 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -59,6 +59,11 @@ export class DuplicateService extends BaseService { return JobStatus.FAILED; } + if (asset.stackId) { + this.logger.debug(`Asset ${id} is part of a stack, skipping`); + return JobStatus.SKIPPED; + } + if (!asset.isVisible) { this.logger.debug(`Asset ${id} is not visible, skipping`); return JobStatus.SKIPPED; diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index 37e58d5863..42010d598c 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -233,10 +233,6 @@ describe(JobService.name, () => { }, { item: { name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } }, - jobs: [JobName.LINK_LIVE_PHOTOS], - }, - { - item: { name: JobName.LINK_LIVE_PHOTOS, data: { id: 'asset-1' } }, jobs: [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE], }, { diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index 95ff1ad303..342aec7a7a 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -70,7 +70,7 @@ export class JobService extends BaseService { } async handleCommand(queueName: QueueName, dto: JobCommandDto): Promise { - this.logger.debug(`Handling command: queue=${queueName},force=${dto.force}`); + this.logger.debug(`Handling command: queue=${queueName},command=${dto.command},force=${dto.force}`); switch (dto.command) { case JobCommand.START: { @@ -170,7 +170,7 @@ export class JobService extends BaseService { } case QueueName.LIBRARY: { - return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, data: { force } }); + return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } }); } case QueueName.BACKUP_DATABASE: { @@ -195,7 +195,11 @@ export class JobService extends BaseService { await this.onDone(job); } } catch (error: Error | any) { - this.logger.error(`Unable to run job handler (${queueName}/${job.name}): ${error}`, error?.stack, job.data); + this.logger.error( + `Unable to run job handler (${queueName}/${job.name}): ${error}`, + error?.stack, + JSON.stringify(job.data), + ); } finally { this.telemetryRepository.jobs.addToGauge(queueMetric, -1); } @@ -251,11 +255,6 @@ export class JobService extends BaseService { this.eventRepository.clientSend('on_asset_update', asset.ownerId, mapAsset(asset)); } } - await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data }); - break; - } - - case JobName.LINK_LIVE_PHOTOS: { await this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: item.data }); break; } diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index ded7e0630a..c869f803f0 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -1079,7 +1079,7 @@ describe(LibraryService.name, () => { it('should queue the refresh job', async () => { mocks.library.getAll.mockResolvedValue([libraryStub.externalLibrary1]); - await expect(sut.handleQueueSyncAll()).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleQueueScanAll()).resolves.toBe(JobStatus.SUCCESS); expect(mocks.job.queue.mock.calls).toEqual([ [ diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 441d130c12..cdd6a3948f 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -47,7 +47,7 @@ export class LibraryService extends BaseService { name: 'libraryScan', expression: scan.cronExpression, onTick: () => - handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL }), this.logger), + handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL }), this.logger), start: scan.enabled, }); } @@ -210,11 +210,17 @@ export class LibraryService extends BaseService { @OnJob({ name: JobName.LIBRARY_QUEUE_CLEANUP, queue: QueueName.LIBRARY }) async handleQueueCleanup(): Promise { - this.logger.debug('Cleaning up any pending library deletions'); - const pendingDeletion = await this.libraryRepository.getAllDeleted(); - await this.jobRepository.queueAll( - pendingDeletion.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })), - ); + this.logger.log('Checking for any libraries pending deletion...'); + const pendingDeletions = await this.libraryRepository.getAllDeleted(); + if (pendingDeletions.length > 0) { + const libraryString = pendingDeletions.length === 1 ? 'library' : 'libraries'; + this.logger.log(`Found ${pendingDeletions.length} ${libraryString} pending deletion, cleaning up...`); + + await this.jobRepository.queueAll( + pendingDeletions.map((libraryToDelete) => ({ name: JobName.LIBRARY_DELETE, data: { id: libraryToDelete.id } })), + ); + } + return JobStatus.SUCCESS; } @@ -442,9 +448,13 @@ export class LibraryService extends BaseService { await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } }); } - @OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, queue: QueueName.LIBRARY }) - async handleQueueSyncAll(): Promise { - this.logger.debug(`Refreshing all external libraries`); + async queueScanAll() { + await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: {} }); + } + + @OnJob({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, queue: QueueName.LIBRARY }) + async handleQueueScanAll(): Promise { + this.logger.log(`Refreshing all external libraries`); await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} }); diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 10b8cee2fe..8a46b289c3 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -30,35 +30,33 @@ export class MemoryService extends BaseService { const start = DateTime.utc().startOf('day').minus({ days: DAYS }); const state = await this.systemMetadataRepository.get(SystemMetadataKey.MEMORIES_STATE); - let lastOnThisDayDate = state?.lastOnThisDayDate ? DateTime.fromISO(state?.lastOnThisDayDate) : start; + const lastOnThisDayDate = state?.lastOnThisDayDate ? DateTime.fromISO(state.lastOnThisDayDate) : start; // generate a memory +/- X days from today - for (let i = 0; i <= DAYS * 2 + 1; i++) { + for (let i = 0; i <= DAYS * 2; i++) { const target = start.plus({ days: i }); - if (lastOnThisDayDate > target) { + if (lastOnThisDayDate >= target) { continue; } const showAt = target.startOf('day').toISO(); const hideAt = target.endOf('day').toISO(); - this.logger.log(`Creating memories for month=${target.month}, day=${target.day}`); - for (const [userId, userIds] of Object.entries(userMap)) { const memories = await this.assetRepository.getByDayOfYear(userIds, target); - for (const memory of memories) { - const data: OnThisDayData = { year: target.year - memory.yearsAgo }; + for (const { year, assets } of memories) { + const data: OnThisDayData = { year }; await this.memoryRepository.create( { ownerId: userId, type: MemoryType.ON_THIS_DAY, data, - memoryAt: target.minus({ years: memory.yearsAgo }).toISO(), + memoryAt: target.set({ year }).toISO(), showAt, hideAt, }, - new Set(memory.assets.map(({ id }) => id)), + new Set(assets.map(({ id }) => id)), ); } } @@ -67,8 +65,6 @@ export class MemoryService extends BaseService { ...state, lastOnThisDayDate: target.toISO(), }); - - lastOnThisDayDate = target; } } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index f5b10aa379..e8ffd4a04f 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -2,6 +2,7 @@ import { BinaryField, ExifDateTime } from 'exiftool-vendored'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; +import { defaults } from 'src/config'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { AssetType, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum'; @@ -54,122 +55,24 @@ describe(MetadataService.name, () => { }); }); - describe('handleLivePhotoLinking', () => { - it('should handle an asset that could not be found', async () => { - await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); - expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); - expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled(); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.album.removeAsset).not.toHaveBeenCalled(); + describe('onConfigInit', () => { + it('should update metadata processing concurrency', () => { + sut.onConfigInit({ newConfig: defaults }); + + expect(mocks.metadata.setMaxConcurrency).toHaveBeenCalledWith(defaults.job.metadataExtraction.concurrency); + expect(mocks.metadata.setMaxConcurrency).toHaveBeenCalledTimes(1); }); + }); - it('should handle an asset without exif info', async () => { - mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.image, exifInfo: undefined }]); + describe('onConfigUpdate', () => { + it('should update metadata processing concurrency', () => { + const newConfig = structuredClone(defaults); + newConfig.job.metadataExtraction.concurrency = 10; - await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.FAILED); - expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); - expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled(); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.album.removeAsset).not.toHaveBeenCalled(); - }); + sut.onConfigUpdate({ oldConfig: defaults, newConfig }); - it('should handle livePhotoCID not set', async () => { - mocks.asset.getByIds.mockResolvedValue([{ ...assetStub.image }]); - - await expect(sut.handleLivePhotoLinking({ id: assetStub.image.id })).resolves.toBe(JobStatus.SKIPPED); - expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { exifInfo: true }); - expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled(); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.album.removeAsset).not.toHaveBeenCalled(); - }); - - it('should handle not finding a match', async () => { - mocks.asset.getByIds.mockResolvedValue([ - { - ...assetStub.livePhotoMotionAsset, - exifInfo: { livePhotoCID: assetStub.livePhotoStillAsset.id } as ExifEntity, - }, - ]); - - await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe( - JobStatus.SKIPPED, - ); - expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { exifInfo: true }); - expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ - livePhotoCID: assetStub.livePhotoStillAsset.id, - ownerId: assetStub.livePhotoMotionAsset.ownerId, - otherAssetId: assetStub.livePhotoMotionAsset.id, - type: AssetType.IMAGE, - }); - expect(mocks.asset.update).not.toHaveBeenCalled(); - expect(mocks.album.removeAsset).not.toHaveBeenCalled(); - }); - - it('should link photo and video', async () => { - mocks.asset.getByIds.mockResolvedValue([ - { - ...assetStub.livePhotoStillAsset, - exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity, - }, - ]); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); - - await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( - JobStatus.SUCCESS, - ); - expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { exifInfo: true }); - expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ - livePhotoCID: assetStub.livePhotoMotionAsset.id, - ownerId: assetStub.livePhotoStillAsset.ownerId, - otherAssetId: assetStub.livePhotoStillAsset.id, - type: AssetType.VIDEO, - }); - expect(mocks.asset.update).toHaveBeenCalledWith({ - id: assetStub.livePhotoStillAsset.id, - livePhotoVideoId: assetStub.livePhotoMotionAsset.id, - }); - expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false }); - expect(mocks.album.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); - }); - - it('should notify clients on live photo link', async () => { - mocks.asset.getByIds.mockResolvedValue([ - { - ...assetStub.livePhotoStillAsset, - exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity, - }, - ]); - mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); - - await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( - JobStatus.SUCCESS, - ); - expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', { - userId: assetStub.livePhotoMotionAsset.ownerId, - assetId: assetStub.livePhotoMotionAsset.id, - }); - }); - - it('should search by libraryId', async () => { - mocks.asset.getByIds.mockResolvedValue([ - { - ...assetStub.livePhotoStillAsset, - libraryId: 'library-id', - exifInfo: { livePhotoCID: 'CID' } as ExifEntity, - }, - ]); - - await expect(sut.handleLivePhotoLinking({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( - JobStatus.SKIPPED, - ); - - expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ - ownerId: 'user-id', - otherAssetId: 'live-photo-still-asset', - livePhotoCID: 'CID', - libraryId: 'library-id', - type: 'VIDEO', - }); + expect(mocks.metadata.setMaxConcurrency).toHaveBeenCalledWith(newConfig.job.metadataExtraction.concurrency); + expect(mocks.metadata.setMaxConcurrency).toHaveBeenCalledTimes(1); }); }); @@ -227,7 +130,6 @@ describe(MetadataService.name, () => { id: assetStub.image.id, duration: null, fileCreatedAt: sidecarDate, - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), localDateTime: sidecarDate, }); }); @@ -247,7 +149,6 @@ describe(MetadataService.name, () => { id: assetStub.image.id, duration: null, fileCreatedAt: fileModifiedAt, - fileModifiedAt, localDateTime: fileModifiedAt, }); }); @@ -265,7 +166,6 @@ describe(MetadataService.name, () => { id: assetStub.image.id, duration: null, fileCreatedAt, - fileModifiedAt, localDateTime: fileCreatedAt, }); }); @@ -300,7 +200,6 @@ describe(MetadataService.name, () => { id: assetStub.image.id, duration: null, fileCreatedAt: assetStub.image.fileCreatedAt, - fileModifiedAt: assetStub.image.fileModifiedAt, localDateTime: assetStub.image.fileCreatedAt, }); }); @@ -323,7 +222,6 @@ describe(MetadataService.name, () => { id: assetStub.withLocation.id, duration: null, fileCreatedAt: assetStub.withLocation.createdAt, - fileModifiedAt: assetStub.withLocation.createdAt, localDateTime: new Date('2023-02-22T05:06:29.716Z'), }); }); @@ -343,7 +241,7 @@ describe(MetadataService.name, () => { it('should extract tags from TagsList', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ TagsList: ['Parent'] }); - mocks.tag.upsertValue.mockResolvedValue(tagStub.parent); + mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -353,27 +251,27 @@ describe(MetadataService.name, () => { it('should extract hierarchy from TagsList', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ TagsList: ['Parent/Child'] }); - mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parent); - mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.child); + mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); + mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', - parent: undefined, + parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { userId: 'user-id', value: 'Parent/Child', - parent: tagStub.parent, + parentId: 'tag-parent', }); }); it('should extract tags from Keywords as a string', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ Keywords: 'Parent' }); - mocks.tag.upsertValue.mockResolvedValue(tagStub.parent); + mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -383,7 +281,7 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a list', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ Keywords: ['Parent'] }); - mocks.tag.upsertValue.mockResolvedValue(tagStub.parent); + mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -393,7 +291,7 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a list with a number', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ Keywords: ['Parent', 2024] }); - mocks.tag.upsertValue.mockResolvedValue(tagStub.parent); + mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -404,58 +302,58 @@ describe(MetadataService.name, () => { it('should extract hierarchal tags from Keywords', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ Keywords: 'Parent/Child' }); - mocks.tag.upsertValue.mockResolvedValue(tagStub.parent); + mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', - parent: undefined, + parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { userId: 'user-id', value: 'Parent/Child', - parent: tagStub.parent, + parentId: 'tag-parent', }); }); it('should ignore Keywords when TagsList is present', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ Keywords: 'Child', TagsList: ['Parent/Child'] }); - mocks.tag.upsertValue.mockResolvedValue(tagStub.parent); + mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', - parent: undefined, + parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { userId: 'user-id', value: 'Parent/Child', - parent: tagStub.parent, + parentId: 'tag-parent', }); }); it('should extract hierarchy from HierarchicalSubject', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ HierarchicalSubject: ['Parent|Child', 'TagA'] }); - mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parent); - mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.child); + mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); + mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', - parent: undefined, + parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { userId: 'user-id', value: 'Parent/Child', - parent: tagStub.parent, + parentId: 'tag-parent', }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(3, { userId: 'user-id', value: 'TagA', parent: undefined }); }); @@ -463,7 +361,7 @@ describe(MetadataService.name, () => { it('should extract tags from HierarchicalSubject as a list with a number', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ HierarchicalSubject: ['Parent', 2024] }); - mocks.tag.upsertValue.mockResolvedValue(tagStub.parent); + mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -474,7 +372,7 @@ describe(MetadataService.name, () => { it('should extract ignore / characters in a HierarchicalSubject tag', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ HierarchicalSubject: ['Mom/Dad'] }); - mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parent); + mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -488,19 +386,19 @@ describe(MetadataService.name, () => { it('should ignore HierarchicalSubject when TagsList is present', async () => { mocks.asset.getByIds.mockResolvedValue([assetStub.image]); mockReadTags({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] }); - mocks.tag.upsertValue.mockResolvedValue(tagStub.parent); + mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', - parent: undefined, + parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { userId: 'user-id', value: 'Parent/Child', - parent: tagStub.parent, + parentId: 'tag-parent', }); }); @@ -510,7 +408,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(mocks.tag.upsertAssetTags).toHaveBeenCalledWith({ assetId: 'asset-id', tagIds: [] }); + expect(mocks.tag.replaceAssetTags).toHaveBeenCalledWith('asset-id', []); }); it('should not apply motion photos if asset is video', async () => { @@ -729,11 +627,11 @@ describe(MetadataService.name, () => { mocks.storage.checkFileExists.mockResolvedValue(true); await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); - expect(mocks.asset.create).toHaveBeenCalledTimes(0); - expect(mocks.storage.createOrOverwriteFile).toHaveBeenCalledTimes(0); + expect(mocks.asset.create).not.toHaveBeenCalled(); + expect(mocks.storage.createOrOverwriteFile).not.toHaveBeenCalled(); // The still asset gets saved by handleMetadataExtraction, but not the video expect(mocks.asset.update).toHaveBeenCalledTimes(1); - expect(mocks.job.queue).toHaveBeenCalledTimes(0); + expect(mocks.job.queue).not.toHaveBeenCalled(); }); it('should link and hide motion video asset to still asset if the hash of the extracted video matches an existing asset', async () => { @@ -845,7 +743,6 @@ describe(MetadataService.name, () => { id: assetStub.image.id, duration: null, fileCreatedAt: dateForTest, - fileModifiedAt: dateForTest, localDateTime: dateForTest, }); }); @@ -1162,6 +1059,107 @@ describe(MetadataService.name, () => { }), ); }); + + it('should handle livePhotoCID not set', async () => { + mocks.asset.getByIds.mockResolvedValue([assetStub.image]); + + await expect(sut.handleMetadataExtraction({ id: assetStub.image.id })).resolves.toBe(JobStatus.SUCCESS); + + expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.image.id], { faces: { person: false } }); + expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled(); + expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false })); + expect(mocks.album.removeAsset).not.toHaveBeenCalled(); + }); + + it('should handle not finding a match', async () => { + mocks.media.probe.mockResolvedValue(probeStub.videoStreamVertical2160p); + mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + mockReadTags({ ContentIdentifier: 'CID' }); + + await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id })).resolves.toBe( + JobStatus.SUCCESS, + ); + + expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id], { + faces: { person: false }, + }); + expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ + livePhotoCID: 'CID', + ownerId: assetStub.livePhotoMotionAsset.ownerId, + otherAssetId: assetStub.livePhotoMotionAsset.id, + type: AssetType.IMAGE, + }); + expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false })); + expect(mocks.album.removeAsset).not.toHaveBeenCalled(); + }); + + it('should link photo and video', async () => { + mocks.asset.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]); + mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + mockReadTags({ ContentIdentifier: 'CID' }); + + await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( + JobStatus.SUCCESS, + ); + + expect(mocks.asset.getByIds).toHaveBeenCalledWith([assetStub.livePhotoStillAsset.id], { + faces: { person: false }, + }); + expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ + livePhotoCID: 'CID', + ownerId: assetStub.livePhotoStillAsset.ownerId, + otherAssetId: assetStub.livePhotoStillAsset.id, + type: AssetType.VIDEO, + }); + expect(mocks.asset.update).toHaveBeenCalledWith({ + id: assetStub.livePhotoStillAsset.id, + livePhotoVideoId: assetStub.livePhotoMotionAsset.id, + }); + expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false }); + expect(mocks.album.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id); + }); + + it('should notify clients on live photo link', async () => { + mocks.asset.getByIds.mockResolvedValue([ + { + ...assetStub.livePhotoStillAsset, + exifInfo: { livePhotoCID: assetStub.livePhotoMotionAsset.id } as ExifEntity, + }, + ]); + mocks.asset.findLivePhotoMatch.mockResolvedValue(assetStub.livePhotoMotionAsset); + mockReadTags({ ContentIdentifier: 'CID' }); + + await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( + JobStatus.SUCCESS, + ); + + expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', { + userId: assetStub.livePhotoMotionAsset.ownerId, + assetId: assetStub.livePhotoMotionAsset.id, + }); + }); + + it('should search by libraryId', async () => { + mocks.asset.getByIds.mockResolvedValue([ + { + ...assetStub.livePhotoStillAsset, + libraryId: 'library-id', + }, + ]); + mockReadTags({ ContentIdentifier: 'CID' }); + + await expect(sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id })).resolves.toBe( + JobStatus.SUCCESS, + ); + + expect(mocks.asset.findLivePhotoMatch).toHaveBeenCalledWith({ + ownerId: 'user-id', + otherAssetId: 'live-photo-still-asset', + livePhotoCID: 'CID', + libraryId: 'library-id', + type: 'VIDEO', + }); + }); }); describe('handleQueueSidecar', () => { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 78ea8089e6..14513c738a 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -89,6 +89,16 @@ export class MetadataService extends BaseService { await this.metadataRepository.teardown(); } + @OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] }) + onConfigInit({ newConfig }: ArgOf<'config.init'>) { + this.metadataRepository.setMaxConcurrency(newConfig.job.metadataExtraction.concurrency); + } + + @OnEvent({ name: 'config.update', workers: [ImmichWorker.MICROSERVICES], server: true }) + onConfigUpdate({ newConfig }: ArgOf<'config.update'>) { + this.metadataRepository.setMaxConcurrency(newConfig.job.metadataExtraction.concurrency); + } + private async init() { this.logger.log('Initializing metadata service'); @@ -103,21 +113,14 @@ export class MetadataService extends BaseService { } } - @OnJob({ name: JobName.LINK_LIVE_PHOTOS, queue: QueueName.METADATA_EXTRACTION }) - async handleLivePhotoLinking(job: JobOf): Promise { - const { id } = job; - const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true }); - if (!asset?.exifInfo) { - return JobStatus.FAILED; - } - - if (!asset.exifInfo.livePhotoCID) { - return JobStatus.SKIPPED; + private async linkLivePhotos(asset: AssetEntity, exifInfo: Insertable): Promise { + if (!exifInfo.livePhotoCID) { + return; } const otherType = asset.type === AssetType.VIDEO ? AssetType.IMAGE : AssetType.VIDEO; const match = await this.assetRepository.findLivePhotoMatch({ - livePhotoCID: asset.exifInfo.livePhotoCID, + livePhotoCID: exifInfo.livePhotoCID, ownerId: asset.ownerId, libraryId: asset.libraryId, otherAssetId: asset.id, @@ -125,18 +128,17 @@ export class MetadataService extends BaseService { }); if (!match) { - return JobStatus.SKIPPED; + return; } const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset]; - - await this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }); - await this.assetRepository.update({ id: motionAsset.id, isVisible: false }); - await this.albumRepository.removeAsset(motionAsset.id); + await Promise.all([ + this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }), + this.assetRepository.update({ id: motionAsset.id, isVisible: false }), + this.albumRepository.removeAsset(motionAsset.id), + ]); await this.eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId: motionAsset.ownerId }); - - return JobStatus.SUCCESS; } @OnJob({ name: JobName.QUEUE_METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION }) @@ -158,9 +160,9 @@ export class MetadataService extends BaseService { } @OnJob({ name: JobName.METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION }) - async handleMetadataExtraction({ id }: JobOf): Promise { + async handleMetadataExtraction(data: JobOf): Promise { const { metadata, reverseGeocoding } = await this.getConfig({ withCache: true }); - const [asset] = await this.assetRepository.getByIds([id], { faces: { person: false } }); + const [asset] = await this.assetRepository.getByIds([data.id], { faces: { person: false } }); if (!asset) { return JobStatus.FAILED; } @@ -238,18 +240,19 @@ export class MetadataService extends BaseService { duration: exifTags.Duration?.toString() ?? null, localDateTime, fileCreatedAt: exifData.dateTimeOriginal ?? undefined, - fileModifiedAt: exifData.modifyDate ?? undefined, + fileModifiedAt: stats.mtime, }); - await this.assetRepository.upsertJobStatus({ - assetId: asset.id, - metadataExtractedAt: new Date(), - }); + if (exifData.livePhotoCID) { + await this.linkLivePhotos(asset, exifData); + } if (isFaceImportEnabled(metadata)) { await this.applyTaggedFaces(asset, exifTags); } + await this.assetRepository.upsertJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() }); + return JobStatus.SUCCESS; } @@ -387,7 +390,10 @@ export class MetadataService extends BaseService { } const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags }); - await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds: results.map((tag) => tag.id) }); + await this.tagRepository.replaceAssetTags( + asset.id, + results.map((tag) => tag.id), + ); } private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags) { diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index dd998cc0fe..e297910a95 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -736,6 +736,7 @@ export class PersonService extends BaseService { boundingBoxX2: dto.x + dto.width, boundingBoxY1: dto.y, boundingBoxY2: dto.y + dto.height, + sourceType: SourceType.MANUAL, }); } diff --git a/server/src/services/session.service.spec.ts b/server/src/services/session.service.spec.ts index 8c22abb7f0..96a1dacf64 100644 --- a/server/src/services/session.service.spec.ts +++ b/server/src/services/session.service.spec.ts @@ -33,6 +33,7 @@ describe('SessionService', () => { id: '123', token: '420', userId: '42', + updateId: 'uuid-v7', }, ]); diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 24a9fcd459..ea8db9857d 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -152,6 +152,7 @@ export class StorageTemplateService extends BaseService { this.logger.log('Storage template migration disabled, skipping'); return JobStatus.SKIPPED; } + await this.moveRepository.cleanMoveHistory(); const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => this.assetRepository.getAll(pagination, { withExif: true, withArchived: true }), ); @@ -175,6 +176,12 @@ export class StorageTemplateService extends BaseService { return JobStatus.SUCCESS; } + @OnEvent({ name: 'asset.delete' }) + async handleMoveHistoryCleanup({ assetId }: ArgOf<'asset.delete'>) { + this.logger.debug(`Cleaning up move history for asset ${assetId}`); + await this.moveRepository.cleanMoveHistorySingle(assetId); + } + async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) { if (asset.isExternal || StorageCore.isAndroidMotionPath(asset.originalPath)) { // External assets are not affected by storage template @@ -304,6 +311,7 @@ export class StorageTemplateService extends BaseService { filetype: asset.type == AssetType.IMAGE ? 'IMG' : 'VID', filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO', assetId: asset.id, + assetIdShort: asset.id.slice(-12), //just throw into the root if it doesn't belong to an album album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '', }; diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index c0c7a00ae7..ca1d9e7921 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -54,7 +54,7 @@ export class StorageService extends BaseService { this.logger.log('Successfully verified system mount folder checks'); } catch (error) { if (envData.storage.ignoreMountCheckErrors) { - this.logger.error(error); + this.logger.error(error as Error); this.logger.warn('Ignoring mount folder errors'); } else { throw error; diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index b94e8cfcbf..45b1b7ff84 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -1,4 +1,4 @@ -import { ForbiddenException, Injectable } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; import { Insertable } from 'kysely'; import { DateTime } from 'luxon'; import { Writable } from 'node:stream'; @@ -25,6 +25,7 @@ const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; const SYNC_TYPES_ORDER = [ // SyncRequestType.UsersV1, + SyncRequestType.PartnersV1, ]; const throwSessionRequired = () => { @@ -43,8 +44,6 @@ export class SyncService extends BaseService { } async setAcks(auth: AuthDto, dto: SyncAckSetDto) { - // TODO ack validation - const sessionId = auth.session?.id; if (!sessionId) { return throwSessionRequired(); @@ -53,6 +52,10 @@ export class SyncService extends BaseService { const checkpoints: Insertable[] = []; for (const ack of dto.acks) { const { type } = fromAck(ack); + // TODO proper ack validation via class validator + if (!Object.values(SyncEntityType).includes(type)) { + throw new BadRequestException(`Invalid ack type: ${type}`); + } checkpoints.push({ sessionId, type, ack }); } @@ -79,19 +82,34 @@ export class SyncService extends BaseService { checkpoints.map(({ type, ack }) => [type, fromAck(ack)]), ); - // TODO pre-filter/sort list based on optimal sync order - for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) { switch (type) { case SyncRequestType.UsersV1: { const deletes = this.syncRepository.getUserDeletes(checkpointMap[SyncEntityType.UserDeleteV1]); - for await (const { ackEpoch, ...data } of deletes) { - response.write(serialize({ type: SyncEntityType.UserDeleteV1, ackEpoch, ids: [data.userId], data })); + for await (const { id, ...data } of deletes) { + response.write(serialize({ type: SyncEntityType.UserDeleteV1, updateId: id, data })); } const upserts = this.syncRepository.getUserUpserts(checkpointMap[SyncEntityType.UserV1]); - for await (const { ackEpoch, ...data } of upserts) { - response.write(serialize({ type: SyncEntityType.UserV1, ackEpoch, ids: [data.id], data })); + for await (const { updateId, ...data } of upserts) { + response.write(serialize({ type: SyncEntityType.UserV1, updateId, data })); + } + + break; + } + + case SyncRequestType.PartnersV1: { + const deletes = this.syncRepository.getPartnerDeletes( + auth.user.id, + checkpointMap[SyncEntityType.PartnerDeleteV1], + ); + for await (const { id, ...data } of deletes) { + response.write(serialize({ type: SyncEntityType.PartnerDeleteV1, updateId: id, data })); + } + + const upserts = this.syncRepository.getPartnerUpserts(auth.user.id, checkpointMap[SyncEntityType.PartnerV1]); + for await (const { updateId, ...data } of upserts) { + response.write(serialize({ type: SyncEntityType.PartnerV1, updateId, data })); } break; diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index f1385eb8c8..22d7747d0d 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -22,7 +22,7 @@ describe(TagService.name, () => { describe('getAll', () => { it('should return all tags for a user', async () => { - mocks.tag.getAll.mockResolvedValue([tagStub.tag1]); + mocks.tag.getAll.mockResolvedValue([tagStub.tag]); await expect(sut.getAll(authStub.admin)).resolves.toEqual([tagResponseStub.tag1]); expect(mocks.tag.getAll).toHaveBeenCalledWith(authStub.admin.user.id); }); @@ -30,13 +30,12 @@ describe(TagService.name, () => { describe('get', () => { it('should throw an error for an invalid id', async () => { - mocks.tag.get.mockResolvedValue(null); await expect(sut.get(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); expect(mocks.tag.get).toHaveBeenCalledWith('tag-1'); }); it('should return a tag for a user', async () => { - mocks.tag.get.mockResolvedValue(tagStub.tag1); + mocks.tag.get.mockResolvedValue(tagStub.tag); await expect(sut.get(authStub.admin, 'tag-1')).resolves.toEqual(tagResponseStub.tag1); expect(mocks.tag.get).toHaveBeenCalledWith('tag-1'); }); @@ -53,9 +52,9 @@ describe(TagService.name, () => { it('should create a tag with a parent', async () => { mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-parent'])); - mocks.tag.create.mockResolvedValue(tagStub.tag1); - mocks.tag.get.mockResolvedValueOnce(tagStub.parent); - mocks.tag.get.mockResolvedValueOnce(tagStub.child); + mocks.tag.create.mockResolvedValue(tagStub.tagCreate); + mocks.tag.get.mockResolvedValueOnce(tagStub.parentUpsert); + mocks.tag.get.mockResolvedValueOnce(tagStub.childUpsert); await expect(sut.create(authStub.admin, { name: 'tagA', parentId: 'tag-parent' })).resolves.toBeDefined(); expect(mocks.tag.create).toHaveBeenCalledWith(expect.objectContaining({ value: 'Parent/tagA' })); }); @@ -71,14 +70,14 @@ describe(TagService.name, () => { describe('create', () => { it('should throw an error for a duplicate tag', async () => { - mocks.tag.getByValue.mockResolvedValue(tagStub.tag1); + mocks.tag.getByValue.mockResolvedValue(tagStub.tag); await expect(sut.create(authStub.admin, { name: 'tag-1' })).rejects.toBeInstanceOf(BadRequestException); expect(mocks.tag.getByValue).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(mocks.tag.create).not.toHaveBeenCalled(); }); it('should create a new tag', async () => { - mocks.tag.create.mockResolvedValue(tagStub.tag1); + mocks.tag.create.mockResolvedValue(tagStub.tagCreate); await expect(sut.create(authStub.admin, { name: 'tag-1' })).resolves.toEqual(tagResponseStub.tag1); expect(mocks.tag.create).toHaveBeenCalledWith({ userId: authStub.admin.user.id, @@ -87,7 +86,7 @@ describe(TagService.name, () => { }); it('should create a new tag with optional color', async () => { - mocks.tag.create.mockResolvedValue(tagStub.color1); + mocks.tag.create.mockResolvedValue(tagStub.colorCreate); await expect(sut.create(authStub.admin, { name: 'tag-1', color: '#000000' })).resolves.toEqual( tagResponseStub.color1, ); @@ -110,15 +109,15 @@ describe(TagService.name, () => { it('should update a tag', async () => { mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1'])); - mocks.tag.update.mockResolvedValue(tagStub.color1); + mocks.tag.update.mockResolvedValue(tagStub.colorCreate); await expect(sut.update(authStub.admin, 'tag-1', { color: '#000000' })).resolves.toEqual(tagResponseStub.color1); - expect(mocks.tag.update).toHaveBeenCalledWith({ id: 'tag-1', color: '#000000' }); + expect(mocks.tag.update).toHaveBeenCalledWith('tag-1', { color: '#000000' }); }); }); describe('upsert', () => { it('should upsert a new tag', async () => { - mocks.tag.upsertValue.mockResolvedValue(tagStub.parent); + mocks.tag.upsertValue.mockResolvedValue(tagStub.parentUpsert); await expect(sut.upsert(authStub.admin, { tags: ['Parent'] })).resolves.toBeDefined(); expect(mocks.tag.upsertValue).toHaveBeenCalledWith({ value: 'Parent', @@ -128,36 +127,34 @@ describe(TagService.name, () => { }); it('should upsert a nested tag', async () => { - mocks.tag.getByValue.mockResolvedValueOnce(null); - mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parent); - mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.child); + mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); + mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); await expect(sut.upsert(authStub.admin, { tags: ['Parent/Child'] })).resolves.toBeDefined(); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { value: 'Parent', userId: 'admin_id', - parent: undefined, + parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { value: 'Parent/Child', userId: 'admin_id', - parent: expect.objectContaining({ id: 'tag-parent' }), + parentId: 'tag-parent', }); }); it('should upsert a tag and ignore leading and trailing slashes', async () => { - mocks.tag.getByValue.mockResolvedValueOnce(null); - mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parent); - mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.child); + mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.parentUpsert); + mocks.tag.upsertValue.mockResolvedValueOnce(tagStub.childUpsert); await expect(sut.upsert(authStub.admin, { tags: ['/Parent/Child/'] })).resolves.toBeDefined(); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(1, { value: 'Parent', userId: 'admin_id', - parent: undefined, + parentId: undefined, }); expect(mocks.tag.upsertValue).toHaveBeenNthCalledWith(2, { value: 'Parent/Child', userId: 'admin_id', - parent: expect.objectContaining({ id: 'tag-parent' }), + parentId: 'tag-parent', }); }); }); @@ -170,7 +167,7 @@ describe(TagService.name, () => { }); it('should remove a tag', async () => { - mocks.tag.get.mockResolvedValue(tagStub.tag1); + mocks.tag.get.mockResolvedValue(tagStub.tag); await sut.remove(authStub.admin, 'tag-1'); expect(mocks.tag.delete).toHaveBeenCalledWith('tag-1'); }); @@ -190,12 +187,12 @@ describe(TagService.name, () => { mocks.access.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1', 'tag-2'])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); mocks.tag.upsertAssetIds.mockResolvedValue([ - { tagId: 'tag-1', assetId: 'asset-1' }, - { tagId: 'tag-1', assetId: 'asset-2' }, - { tagId: 'tag-1', assetId: 'asset-3' }, - { tagId: 'tag-2', assetId: 'asset-1' }, - { tagId: 'tag-2', assetId: 'asset-2' }, - { tagId: 'tag-2', assetId: 'asset-3' }, + { tagsId: 'tag-1', assetsId: 'asset-1' }, + { tagsId: 'tag-1', assetsId: 'asset-2' }, + { tagsId: 'tag-1', assetsId: 'asset-3' }, + { tagsId: 'tag-2', assetsId: 'asset-1' }, + { tagsId: 'tag-2', assetsId: 'asset-2' }, + { tagsId: 'tag-2', assetsId: 'asset-3' }, ]); await expect( sut.bulkTagAssets(authStub.admin, { tagIds: ['tag-1', 'tag-2'], assetIds: ['asset-1', 'asset-2', 'asset-3'] }), @@ -203,19 +200,18 @@ describe(TagService.name, () => { count: 6, }); expect(mocks.tag.upsertAssetIds).toHaveBeenCalledWith([ - { tagId: 'tag-1', assetId: 'asset-1' }, - { tagId: 'tag-1', assetId: 'asset-2' }, - { tagId: 'tag-1', assetId: 'asset-3' }, - { tagId: 'tag-2', assetId: 'asset-1' }, - { tagId: 'tag-2', assetId: 'asset-2' }, - { tagId: 'tag-2', assetId: 'asset-3' }, + { tagsId: 'tag-1', assetsId: 'asset-1' }, + { tagsId: 'tag-1', assetsId: 'asset-2' }, + { tagsId: 'tag-1', assetsId: 'asset-3' }, + { tagsId: 'tag-2', assetsId: 'asset-1' }, + { tagsId: 'tag-2', assetsId: 'asset-2' }, + { tagsId: 'tag-2', assetsId: 'asset-3' }, ]); }); }); describe('addAssets', () => { it('should handle invalid ids', async () => { - mocks.tag.get.mockResolvedValue(null); mocks.tag.getAssetIds.mockResolvedValue(new Set([])); await expect(sut.addAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([ { id: 'asset-1', success: false, error: 'no_permission' }, @@ -225,7 +221,7 @@ describe(TagService.name, () => { }); it('should accept accept ids that are new and reject the rest', async () => { - mocks.tag.get.mockResolvedValue(tagStub.tag1); + mocks.tag.get.mockResolvedValue(tagStub.tag); mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1'])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2'])); @@ -245,7 +241,6 @@ describe(TagService.name, () => { describe('removeAssets', () => { it('should throw an error for an invalid id', async () => { - mocks.tag.get.mockResolvedValue(null); mocks.tag.getAssetIds.mockResolvedValue(new Set()); await expect(sut.removeAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([ { id: 'asset-1', success: false, error: 'not_found' }, @@ -253,7 +248,7 @@ describe(TagService.name, () => { }); it('should accept accept ids that are tagged and reject the rest', async () => { - mocks.tag.get.mockResolvedValue(tagStub.tag1); + mocks.tag.get.mockResolvedValue(tagStub.tag); mocks.tag.getAssetIds.mockResolvedValue(new Set(['asset-1'])); await expect( diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index c241f59a80..ecf4d6e9fb 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -1,4 +1,6 @@ import { BadRequestException, Injectable } from '@nestjs/common'; +import { Insertable } from 'kysely'; +import { TagAsset } from 'src/db'; import { OnJob } from 'src/decorators'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -11,9 +13,7 @@ import { TagUpsertDto, mapTag, } from 'src/dtos/tag.dto'; -import { TagEntity } from 'src/entities/tag.entity'; import { JobName, JobStatus, Permission, QueueName } from 'src/enum'; -import { AssetTagItem } from 'src/repositories/tag.repository'; import { BaseService } from 'src/services/base.service'; import { addAssets, removeAssets } from 'src/utils/asset.util'; import { upsertTags } from 'src/utils/tag'; @@ -32,10 +32,10 @@ export class TagService extends BaseService { } async create(auth: AuthDto, dto: TagCreateDto) { - let parent: TagEntity | undefined; + let parent; if (dto.parentId) { await this.requireAccess({ auth, permission: Permission.TAG_READ, ids: [dto.parentId] }); - parent = (await this.tagRepository.get(dto.parentId)) || undefined; + parent = await this.tagRepository.get(dto.parentId); if (!parent) { throw new BadRequestException('Tag not found'); } @@ -49,7 +49,7 @@ export class TagService extends BaseService { } const { color } = dto; - const tag = await this.tagRepository.create({ userId, value, color, parent }); + const tag = await this.tagRepository.create({ userId, value, color, parentId: parent?.id }); return mapTag(tag); } @@ -58,7 +58,7 @@ export class TagService extends BaseService { await this.requireAccess({ auth, permission: Permission.TAG_UPDATE, ids: [id] }); const { color } = dto; - const tag = await this.tagRepository.update({ id, color }); + const tag = await this.tagRepository.update(id, { color }); return mapTag(tag); } @@ -81,15 +81,15 @@ export class TagService extends BaseService { this.checkAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }), ]); - const items: AssetTagItem[] = []; - for (const tagId of tagIds) { - for (const assetId of assetIds) { - items.push({ tagId, assetId }); + const items: Insertable[] = []; + for (const tagsId of tagIds) { + for (const assetsId of assetIds) { + items.push({ tagsId, assetsId }); } } const results = await this.tagRepository.upsertAssetIds(items); - for (const assetId of new Set(results.map((item) => item.assetId))) { + for (const assetId of new Set(results.map((item) => item.assetsId))) { await this.eventRepository.emit('asset.tag', { assetId }); } diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index c5240b82c1..ee28a20d4d 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -25,11 +25,24 @@ export class VersionService extends BaseService { await this.handleVersionCheck(); await this.databaseRepository.withLock(DatabaseLock.VersionHistory, async () => { - const latest = await this.versionRepository.getLatest(); + const previous = await this.versionRepository.getLatest(); const current = serverVersion.toString(); - if (!latest || latest.version !== current) { - this.logger.log(`Version has changed, adding ${current} to history`); + + if (!previous) { await this.versionRepository.create({ version: current }); + return; + } + + if (previous.version !== current) { + const previousVersion = new SemVer(previous.version); + + this.logger.log(`Adding ${current} to upgrade history`); + await this.versionRepository.create({ version: current }); + + const needsNewMemories = semver.lt(previousVersion, '1.129.0'); + if (needsNewMemories) { + await this.jobRepository.queue({ name: JobName.MEMORIES_CREATE }); + } } }); } diff --git a/server/src/types.ts b/server/src/types.ts index 3aa7a14add..41d98f9a02 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -37,6 +37,15 @@ export type MemoryItem = export type SessionItem = Awaited>[0]; +export type TagItem = { + id: string; + value: string; + createdAt: Date; + updatedAt: Date; + color: string | null; + parentId: string | null; +}; + export interface CropOptions { top: number; left: number; @@ -303,7 +312,6 @@ export type JobItem = // Metadata Extraction | { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob } | { name: JobName.METADATA_EXTRACTION; data: IEntityJob } - | { name: JobName.LINK_LIVE_PHOTOS; data: IEntityJob } // Sidecar Scanning | { name: JobName.QUEUE_SIDECAR; data: IBaseJob } | { name: JobName.SIDECAR_DISCOVERY; data: IEntityJob } @@ -351,7 +359,7 @@ export type JobItem = | { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob } | { name: JobName.LIBRARY_SYNC_ASSET; data: ILibraryAssetJob } | { name: JobName.LIBRARY_DELETE; data: IEntityJob } - | { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob } + | { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data?: IBaseJob } | { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob } // Notification @@ -421,6 +429,5 @@ export interface IBulkAsset { export type SyncAck = { type: SyncEntityType; - ackEpoch: string; - ids: string[]; + updateId: string; }; diff --git a/server/src/utils/pagination.ts b/server/src/utils/pagination.ts index 7cb31d1e04..eb4106c86a 100644 --- a/server/src/utils/pagination.ts +++ b/server/src/utils/pagination.ts @@ -10,6 +10,7 @@ export interface PaginationResult { export type Paginated = Promise>; +/** @deprecated use `this.db. ... .stream()` instead */ export async function* usePagination( pageSize: number, getNextPage: (pagination: PaginationOptions) => PaginationResult | Paginated, diff --git a/server/src/utils/sync.ts b/server/src/utils/sync.ts index 8e426ab860..cfb6660bdc 100644 --- a/server/src/utils/sync.ts +++ b/server/src/utils/sync.ts @@ -9,22 +9,20 @@ type Impossible = { type Exact = U & Impossible>; export const fromAck = (ack: string): SyncAck => { - const [type, timestamp, ...ids] = ack.split('|'); - return { type: type as SyncEntityType, ackEpoch: timestamp, ids }; + const [type, updateId] = ack.split('|'); + return { type: type as SyncEntityType, updateId }; }; -export const toAck = ({ type, ackEpoch, ids }: SyncAck) => [type, ackEpoch, ...ids].join('|'); +export const toAck = ({ type, updateId }: SyncAck) => [type, updateId].join('|'); export const mapJsonLine = (object: unknown) => JSON.stringify(object) + '\n'; export const serialize = ({ type, - ackEpoch, - ids, + updateId, data, }: { type: T; - ackEpoch: string; - ids: string[]; + updateId: string; data: Exact; -}) => mapJsonLine({ type, data, ack: toAck({ type, ackEpoch, ids }) }); +}) => mapJsonLine({ type, data, ack: toAck({ type, updateId }) }); diff --git a/server/src/utils/tag.ts b/server/src/utils/tag.ts index 4b3b360a8b..b095fcfd85 100644 --- a/server/src/utils/tag.ts +++ b/server/src/utils/tag.ts @@ -1,19 +1,19 @@ -import { TagEntity } from 'src/entities/tag.entity'; import { TagRepository } from 'src/repositories/tag.repository'; +import { TagItem } from 'src/types'; type UpsertRequest = { userId: string; tags: string[] }; export const upsertTags = async (repository: TagRepository, { userId, tags }: UpsertRequest) => { tags = [...new Set(tags)]; - const results: TagEntity[] = []; + const results: TagItem[] = []; for (const tag of tags) { const parts = tag.split('/').filter(Boolean); - let parent: TagEntity | undefined; + let parent: TagItem | undefined; for (const part of parts) { const value = parent ? `${parent.value}/${part}` : part; - parent = await repository.upsertValue({ userId, value, parent }); + parent = await repository.upsertValue({ userId, value, parentId: parent?.id }); } if (parent) { diff --git a/server/test/factory.ts b/server/test/factory.ts index 983b7cbb77..8811b08628 100644 --- a/server/test/factory.ts +++ b/server/test/factory.ts @@ -1,14 +1,41 @@ import { Insertable, Kysely } from 'kysely'; import { randomBytes, randomUUID } from 'node:crypto'; import { Writable } from 'node:stream'; -import { Assets, DB, Sessions, Users } from 'src/db'; +import { Assets, DB, Partners, Sessions, Users } from 'src/db'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetType } from 'src/enum'; +import { AccessRepository } from 'src/repositories/access.repository'; +import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumRepository } from 'src/repositories/album.repository'; +import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; +import { AuditRepository } from 'src/repositories/audit.repository'; +import { ConfigRepository } from 'src/repositories/config.repository'; +import { LibraryRepository } from 'src/repositories/library.repository'; +import { LoggingRepository } from 'src/repositories/logging.repository'; +import { MachineLearningRepository } from 'src/repositories/machine-learning.repository'; +import { MediaRepository } from 'src/repositories/media.repository'; +import { MetadataRepository } from 'src/repositories/metadata.repository'; +import { MoveRepository } from 'src/repositories/move.repository'; +import { NotificationRepository } from 'src/repositories/notification.repository'; +import { OAuthRepository } from 'src/repositories/oauth.repository'; +import { PartnerRepository } from 'src/repositories/partner.repository'; +import { PersonRepository } from 'src/repositories/person.repository'; +import { ProcessRepository } from 'src/repositories/process.repository'; +import { SearchRepository } from 'src/repositories/search.repository'; +import { ServerInfoRepository } from 'src/repositories/server-info.repository'; import { SessionRepository } from 'src/repositories/session.repository'; +import { SharedLinkRepository } from 'src/repositories/shared-link.repository'; +import { StackRepository } from 'src/repositories/stack.repository'; +import { StorageRepository } from 'src/repositories/storage.repository'; import { SyncRepository } from 'src/repositories/sync.repository'; +import { TelemetryRepository } from 'src/repositories/telemetry.repository'; +import { TrashRepository } from 'src/repositories/trash.repository'; import { UserRepository } from 'src/repositories/user.repository'; +import { VersionHistoryRepository } from 'src/repositories/version-history.repository'; +import { ViewRepository } from 'src/repositories/view-repository'; +import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; +import { newTelemetryRepositoryMock } from 'test/repositories/telemetry.repository.mock'; class CustomWritable extends Writable { private data = ''; @@ -30,6 +57,7 @@ class CustomWritable extends Writable { type Asset = Insertable; type User = Partial>; type Session = Omit, 'token'> & { token?: string }; +type Partner = Insertable; export const newUuid = () => randomUUID() as string; @@ -37,6 +65,7 @@ export class TestFactory { private assets: Asset[] = []; private sessions: Session[] = []; private users: User[] = []; + private partners: Partner[] = []; private constructor(private context: TestContext) {} @@ -100,6 +129,17 @@ export class TestFactory { }; } + static partner(partner: Partner) { + const defaults = { + inTimeline: true, + }; + + return { + ...defaults, + ...partner, + }; + } + withAsset(asset: Asset) { this.assets.push(asset); return this; @@ -115,6 +155,11 @@ export class TestFactory { return this; } + withPartner(partner: Partner) { + this.partners.push(partner); + return this; + } + async create() { for (const asset of this.assets) { await this.context.createAsset(asset); @@ -124,6 +169,10 @@ export class TestFactory { await this.context.createUser(user); } + for (const partner of this.partners) { + await this.context.createPartner(partner); + } + for (const session of this.sessions) { await this.context.createSession(session); } @@ -133,18 +182,71 @@ export class TestFactory { } export class TestContext { - userRepository: UserRepository; - assetRepository: AssetRepository; - albumRepository: AlbumRepository; - sessionRepository: SessionRepository; - syncRepository: SyncRepository; + access: AccessRepository; + logger: LoggingRepository; + activity: ActivityRepository; + album: AlbumRepository; + apiKey: ApiKeyRepository; + asset: AssetRepository; + audit: AuditRepository; + config: ConfigRepository; + library: LibraryRepository; + machineLearning: MachineLearningRepository; + media: MediaRepository; + metadata: MetadataRepository; + move: MoveRepository; + notification: NotificationRepository; + oauth: OAuthRepository; + partner: PartnerRepository; + person: PersonRepository; + process: ProcessRepository; + search: SearchRepository; + serverInfo: ServerInfoRepository; + session: SessionRepository; + sharedLink: SharedLinkRepository; + stack: StackRepository; + storage: StorageRepository; + sync: SyncRepository; + telemetry: TelemetryRepository; + trash: TrashRepository; + user: UserRepository; + versionHistory: VersionHistoryRepository; + view: ViewRepository; private constructor(private db: Kysely) { - this.userRepository = new UserRepository(this.db); - this.assetRepository = new AssetRepository(this.db); - this.albumRepository = new AlbumRepository(this.db); - this.sessionRepository = new SessionRepository(this.db); - this.syncRepository = new SyncRepository(this.db); + const logger = newLoggingRepositoryMock() as unknown as LoggingRepository; + const config = new ConfigRepository(); + + this.access = new AccessRepository(this.db); + this.logger = logger; + this.activity = new ActivityRepository(this.db); + this.album = new AlbumRepository(this.db); + this.apiKey = new ApiKeyRepository(this.db); + this.asset = new AssetRepository(this.db); + this.audit = new AuditRepository(this.db); + this.config = config; + this.library = new LibraryRepository(this.db); + this.machineLearning = new MachineLearningRepository(logger); + this.media = new MediaRepository(logger); + this.metadata = new MetadataRepository(logger); + this.move = new MoveRepository(this.db); + this.notification = new NotificationRepository(logger); + this.oauth = new OAuthRepository(logger); + this.partner = new PartnerRepository(this.db); + this.person = new PersonRepository(this.db); + this.process = new ProcessRepository(logger); + this.search = new SearchRepository(logger, this.db); + this.serverInfo = new ServerInfoRepository(config, logger); + this.session = new SessionRepository(this.db); + this.sharedLink = new SharedLinkRepository(this.db); + this.stack = new StackRepository(this.db); + this.storage = new StorageRepository(logger); + this.sync = new SyncRepository(this.db); + this.telemetry = newTelemetryRepositoryMock() as unknown as TelemetryRepository; + this.trash = new TrashRepository(this.db); + this.user = new UserRepository(this.db); + this.versionHistory = new VersionHistoryRepository(this.db); + this.view = new ViewRepository(this.db); } static from(db: Kysely) { @@ -156,14 +258,18 @@ export class TestContext { } createUser(user: User = {}) { - return this.userRepository.create(TestFactory.user(user)); + return this.user.create(TestFactory.user(user)); + } + + createPartner(partner: Partner) { + return this.partner.create(TestFactory.partner(partner)); } createAsset(asset: Asset) { - return this.assetRepository.create(TestFactory.asset(asset)); + return this.asset.create(TestFactory.asset(asset)); } createSession(session: Session) { - return this.sessionRepository.create(TestFactory.session(session)); + return this.session.create(TestFactory.session(session)); } } diff --git a/server/test/fixtures/activity.stub.ts b/server/test/fixtures/activity.stub.ts index 9578bcd4a1..a81fd51ca8 100644 --- a/server/test/fixtures/activity.stub.ts +++ b/server/test/fixtures/activity.stub.ts @@ -19,6 +19,7 @@ export const activityStub = { albumId: albumStub.oneAsset.id, createdAt: new Date(), updatedAt: new Date(), + updateId: 'uuid-v7', }), liked: Object.freeze({ id: 'activity-2', @@ -36,5 +37,6 @@ export const activityStub = { albumId: albumStub.oneAsset.id, createdAt: new Date(), updatedAt: new Date(), + updateId: 'uuid-v7', }), }; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 6c20a765c7..a0619f1a10 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -184,6 +184,7 @@ export const assetStub = { exifImageHeight: 1000, exifImageWidth: 1000, } as ExifEntity, + stackId: 'stack-1', stack: stackStub('stack-1', [ { id: 'primary-asset-id' } as AssetEntity, { id: 'stack-child-asset-1' } as AssetEntity, diff --git a/server/test/fixtures/session.stub.ts b/server/test/fixtures/session.stub.ts index cdf499c8d1..af06237473 100644 --- a/server/test/fixtures/session.stub.ts +++ b/server/test/fixtures/session.stub.ts @@ -11,6 +11,7 @@ export const sessionStub = { updatedAt: new Date(), deviceType: '', deviceOS: '', + updateId: 'uuid-v7', }), inactive: Object.freeze({ id: 'not_active', @@ -21,5 +22,6 @@ export const sessionStub = { updatedAt: new Date('2021-01-01'), deviceType: 'Mobile', deviceOS: 'Android', + updateId: 'uuid-v7', }), }; diff --git a/server/test/fixtures/tag.stub.ts b/server/test/fixtures/tag.stub.ts index b245bfe9e5..1a19c2a002 100644 --- a/server/test/fixtures/tag.stub.ts +++ b/server/test/fixtures/tag.stub.ts @@ -1,49 +1,51 @@ import { TagResponseDto } from 'src/dtos/tag.dto'; -import { TagEntity } from 'src/entities/tag.entity'; -import { userStub } from 'test/fixtures/user.stub'; +import { TagItem } from 'src/types'; -const parent = Object.freeze({ +const parent = Object.freeze({ id: 'tag-parent', createdAt: new Date('2021-01-01T00:00:00Z'), updatedAt: new Date('2021-01-01T00:00:00Z'), value: 'Parent', color: null, - userId: userStub.admin.id, - user: userStub.admin, + parentId: null, }); -const child = Object.freeze({ +const child = Object.freeze({ id: 'tag-child', createdAt: new Date('2021-01-01T00:00:00Z'), updatedAt: new Date('2021-01-01T00:00:00Z'), value: 'Parent/Child', color: null, - parent, - userId: userStub.admin.id, - user: userStub.admin, + parentId: parent.id, }); +const tag = { + id: 'tag-1', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + value: 'Tag1', + color: null, + parentId: null, +}; + +const color = { + id: 'tag-1', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + value: 'Tag1', + color: '#000000', + parentId: null, +}; + +const upsert = { userId: 'tag-user', updateId: 'uuid-v7' }; + export const tagStub = { - tag1: Object.freeze({ - id: 'tag-1', - createdAt: new Date('2021-01-01T00:00:00Z'), - updatedAt: new Date('2021-01-01T00:00:00Z'), - value: 'Tag1', - color: null, - userId: userStub.admin.id, - user: userStub.admin, - }), - parent, - child, - color1: Object.freeze({ - id: 'tag-1', - createdAt: new Date('2021-01-01T00:00:00Z'), - updatedAt: new Date('2021-01-01T00:00:00Z'), - value: 'Tag1', - color: '#000000', - userId: userStub.admin.id, - user: userStub.admin, - }), + tag, + tagCreate: { ...tag, ...upsert }, + color, + colorCreate: { ...color, ...upsert }, + parentUpsert: { ...parent, ...upsert }, + childUpsert: { ...child, ...upsert }, }; export const tagResponseStub = { diff --git a/server/test/medium/specs/metadata.service.spec.ts b/server/test/medium/specs/metadata.service.spec.ts index 4c89ce4e37..22b9174ccd 100644 --- a/server/test/medium/specs/metadata.service.spec.ts +++ b/server/test/medium/specs/metadata.service.spec.ts @@ -3,15 +3,12 @@ import { writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { AssetEntity } from 'src/entities/asset.entity'; -import { LoggingRepository } from 'src/repositories/logging.repository'; import { MetadataRepository } from 'src/repositories/metadata.repository'; import { MetadataService } from 'src/services/metadata.service'; -import { ILoggingRepository, newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock'; +import { newFakeLoggingRepository } from 'test/repositories/logger.repository.mock'; import { newRandomImage, newTestService, ServiceMocks } from 'test/utils'; -const metadataRepository = new MetadataRepository( - newLoggingRepositoryMock() as ILoggingRepository as LoggingRepository, -); +const metadataRepository = new MetadataRepository(newFakeLoggingRepository()); const createTestFile = async (exifData: Record) => { const data = newRandomImage(); @@ -37,7 +34,7 @@ describe(MetadataService.name, () => { let mocks: ServiceMocks; beforeEach(() => { - ({ sut, mocks } = newTestService(MetadataService, { metadataRepository })); + ({ sut, mocks } = newTestService(MetadataService, { metadata: metadataRepository })); mocks.storage.stat.mockResolvedValue({ size: 123_456 } as Stats); diff --git a/server/test/medium/specs/sync.service.spec.ts b/server/test/medium/specs/sync.service.spec.ts index bab9794100..b33b010258 100644 --- a/server/test/medium/specs/sync.service.spec.ts +++ b/server/test/medium/specs/sync.service.spec.ts @@ -17,6 +17,8 @@ const setup = async () => { const testSync = async (auth: AuthDto, types: SyncRequestType[]) => { const stream = TestFactory.stream(); + // Wait for 1ms to ensure all updates are available + await new Promise((resolve) => setTimeout(resolve, 1)); await sut.stream(auth, stream, { types }); return stream.getResponse(); @@ -35,7 +37,7 @@ describe(SyncService.name, () => { it('should detect and sync the first user', async () => { const { context, auth, sut, testSync } = await setup(); - const user = await context.userRepository.get(auth.user.id, { withDeleted: false }); + const user = await context.user.get(auth.user.id, { withDeleted: false }); if (!user) { expect.fail('First user should exist'); } @@ -107,7 +109,7 @@ describe(SyncService.name, () => { const { auth, context, sut, testSync } = await setup(); const user = await context.createUser(); - await context.userRepository.delete({ id: user.id }, true); + await context.user.delete({ id: user.id }, true); const response = await testSync(auth, [SyncRequestType.UsersV1]); @@ -165,7 +167,7 @@ describe(SyncService.name, () => { const acks = [initialSyncResponse[0].ack]; await sut.setAcks(auth, { acks }); - const updated = await context.userRepository.update(auth.user.id, { name: 'new name' }); + const updated = await context.user.update(auth.user.id, { name: 'new name' }); const updatedSyncResponse = await testSync(auth, [SyncRequestType.UsersV1]); @@ -186,4 +188,178 @@ describe(SyncService.name, () => { ); }); }); + + describe.concurrent('partners', () => { + it('should detect and sync the first partner', async () => { + const { auth, context, sut, testSync } = await setup(); + + const user1 = auth.user; + const user2 = await context.createUser(); + + const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id }); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(initialSyncResponse).toHaveLength(1); + expect(initialSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + inTimeline: partner.inTimeline, + sharedById: partner.sharedById, + sharedWithId: partner.sharedWithId, + }, + type: 'PartnerV1', + }, + ]), + ); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should detect and sync a deleted partner', async () => { + const { auth, context, sut, testSync } = await setup(); + + const user1 = auth.user; + const user2 = await context.createUser(); + + const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id }); + await context.partner.remove(partner); + + const response = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(response).toHaveLength(1); + expect(response).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + sharedById: partner.sharedById, + sharedWithId: partner.sharedWithId, + }, + type: 'PartnerDeleteV1', + }, + ]), + ); + + const acks = response.map(({ ack }) => ack); + await sut.setAcks(auth, { acks }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should detect and sync a partner share both to and from another user', async () => { + const { auth, context, sut, testSync } = await setup(); + + const user1 = auth.user; + const user2 = await context.createUser(); + + const partner1 = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id }); + const partner2 = await context.createPartner({ sharedById: user1.id, sharedWithId: user2.id }); + + const response = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(response).toHaveLength(2); + expect(response).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + inTimeline: partner1.inTimeline, + sharedById: partner1.sharedById, + sharedWithId: partner1.sharedWithId, + }, + type: 'PartnerV1', + }, + { + ack: expect.any(String), + data: { + inTimeline: partner2.inTimeline, + sharedById: partner2.sharedById, + sharedWithId: partner2.sharedWithId, + }, + type: 'PartnerV1', + }, + ]), + ); + + await sut.setAcks(auth, { acks: [response[1].ack] }); + + const ackSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(ackSyncResponse).toHaveLength(0); + }); + + it('should sync a partner and then an update to that same partner', async () => { + const { auth, context, sut, testSync } = await setup(); + + const user1 = auth.user; + const user2 = await context.createUser(); + + const partner = await context.createPartner({ sharedById: user2.id, sharedWithId: user1.id }); + + const initialSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(initialSyncResponse).toHaveLength(1); + expect(initialSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + inTimeline: partner.inTimeline, + sharedById: partner.sharedById, + sharedWithId: partner.sharedWithId, + }, + type: 'PartnerV1', + }, + ]), + ); + + const acks = [initialSyncResponse[0].ack]; + await sut.setAcks(auth, { acks }); + + const updated = await context.partner.update( + { sharedById: partner.sharedById, sharedWithId: partner.sharedWithId }, + { inTimeline: true }, + ); + + const updatedSyncResponse = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(updatedSyncResponse).toHaveLength(1); + expect(updatedSyncResponse).toEqual( + expect.arrayContaining([ + { + ack: expect.any(String), + data: { + inTimeline: updated.inTimeline, + sharedById: updated.sharedById, + sharedWithId: updated.sharedWithId, + }, + type: 'PartnerV1', + }, + ]), + ); + }); + + it('should not sync a partner for an unrelated user', async () => { + const { auth, context, testSync } = await setup(); + + const user2 = await context.createUser(); + const user3 = await context.createUser(); + + await context.createPartner({ sharedById: user2.id, sharedWithId: user3.id }); + + const response = await testSync(auth, [SyncRequestType.PartnersV1]); + + expect(response).toHaveLength(0); + }); + }); }); diff --git a/server/test/medium/specs/version.service.spec.ts b/server/test/medium/specs/version.service.spec.ts new file mode 100644 index 0000000000..5be36b26ba --- /dev/null +++ b/server/test/medium/specs/version.service.spec.ts @@ -0,0 +1,56 @@ +import { serverVersion } from 'src/constants'; +import { JobName } from 'src/enum'; +import { VersionService } from 'src/services/version.service'; +import { TestContext } from 'test/factory'; +import { getKyselyDB, newTestService } from 'test/utils'; + +const setup = async () => { + const db = await getKyselyDB(); + const context = await TestContext.from(db).create(); + const { sut, mocks } = newTestService(VersionService, context); + + return { + context, + sut, + jobMock: mocks.job, + }; +}; + +describe(VersionService.name, () => { + describe.concurrent('onBootstrap', () => { + it('record the current version on startup', async () => { + const { context, sut } = await setup(); + + const itemsBefore = await context.versionHistory.getAll(); + expect(itemsBefore).toHaveLength(0); + + await sut.onBootstrap(); + + const itemsAfter = await context.versionHistory.getAll(); + expect(itemsAfter).toHaveLength(1); + expect(itemsAfter[0]).toEqual({ + createdAt: expect.any(Date), + id: expect.any(String), + version: serverVersion.toString(), + }); + }); + + it('should queue memory creation when upgrading from 1.128.0', async () => { + const { context, jobMock, sut } = await setup(); + + await context.versionHistory.create({ version: 'v1.128.0' }); + await sut.onBootstrap(); + + expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.MEMORIES_CREATE }); + }); + + it('should not queue memory creation when upgrading from 1.129.0', async () => { + const { context, jobMock, sut } = await setup(); + + await context.versionHistory.create({ version: 'v1.129.0' }); + await sut.onBootstrap(); + + expect(jobMock.queue).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/server/test/repositories/download.repository.mock.ts b/server/test/repositories/download.repository.mock.ts new file mode 100644 index 0000000000..50824c4f3d --- /dev/null +++ b/server/test/repositories/download.repository.mock.ts @@ -0,0 +1,12 @@ +import { DownloadRepository } from 'src/repositories/download.repository'; +import { RepositoryInterface } from 'src/types'; +import { Mocked, vitest } from 'vitest'; + +export const newDownloadRepositoryMock = (): Mocked> => { + return { + downloadAssetIds: vitest.fn(), + downloadMotionAssetIds: vitest.fn(), + downloadAlbumId: vitest.fn(), + downloadUserId: vitest.fn(), + }; +}; diff --git a/server/test/repositories/logger.repository.mock.ts b/server/test/repositories/logger.repository.mock.ts index 46a81c8965..7257d375f1 100644 --- a/server/test/repositories/logger.repository.mock.ts +++ b/server/test/repositories/logger.repository.mock.ts @@ -1,31 +1,23 @@ import { LoggingRepository } from 'src/repositories/logging.repository'; +import { RepositoryInterface } from 'src/types'; import { Mocked, vitest } from 'vitest'; -export type ILoggingRepository = Pick< - LoggingRepository, - | 'verbose' - | 'log' - | 'debug' - | 'warn' - | 'error' - | 'fatal' - | 'isLevelEnabled' - | 'setLogLevel' - | 'setContext' - | 'setAppName' ->; - -export const newLoggingRepositoryMock = (): Mocked => { +export const newLoggingRepositoryMock = (): Mocked> => { return { setLogLevel: vitest.fn(), setContext: vitest.fn(), setAppName: vitest.fn(), isLevelEnabled: vitest.fn(), verbose: vitest.fn(), + verboseFn: vitest.fn(), debug: vitest.fn(), + debugFn: vitest.fn(), log: vitest.fn(), warn: vitest.fn(), error: vitest.fn(), fatal: vitest.fn(), }; }; + +export const newFakeLoggingRepository = () => + newLoggingRepositoryMock() as RepositoryInterface as LoggingRepository; diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index 47a0471b22..854f13b841 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -4,6 +4,7 @@ import { Mocked, vitest } from 'vitest'; export const newMetadataRepositoryMock = (): Mocked> => { return { + setMaxConcurrency: vitest.fn(), teardown: vitest.fn(), readTags: vitest.fn(), writeTags: vitest.fn(), diff --git a/server/test/repositories/move.repository.mock.ts b/server/test/repositories/move.repository.mock.ts index cf304b591e..88dfa29d4f 100644 --- a/server/test/repositories/move.repository.mock.ts +++ b/server/test/repositories/move.repository.mock.ts @@ -8,5 +8,7 @@ export const newMoveRepositoryMock = (): Mocked = T extends RepositoryInterface ? U : never; +type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface }; + +export type ServiceMocks = { + [K in keyof Omit]: Mocked>; +} & { access: IAccessRepositoryMock; telemetry: ITelemetryRepositoryMock }; + type BaseServiceArgs = ConstructorParameters; type Constructor> = { new (...deps: Args): Type; }; -type IAccessRepository = { [K in keyof AccessRepository]: RepositoryInterface }; - -export type ServiceMocks = { - access: IAccessRepositoryMock; - activity: Mocked>; - album: Mocked>; - albumUser: Mocked>; - apiKey: Mocked>; - audit: Mocked>; - asset: Mocked>; - config: Mocked>; - cron: Mocked>; - crypto: Mocked>; - database: Mocked>; - event: Mocked>; - job: Mocked>; - library: Mocked>; - logger: Mocked; - machineLearning: Mocked>; - map: Mocked>; - media: Mocked>; - memory: Mocked>; - metadata: Mocked>; - move: Mocked>; - notification: Mocked>; - oauth: Mocked>; - partner: Mocked>; - person: Mocked>; - process: Mocked>; - search: Mocked>; - serverInfo: Mocked>; - session: Mocked>; - sharedLink: Mocked>; - stack: Mocked>; - storage: Mocked>; - systemMetadata: Mocked>; - tag: Mocked>; - telemetry: ITelemetryRepositoryMock; - trash: Mocked>; - user: Mocked>; - versionHistory: Mocked>; - view: Mocked>; -}; - export const newTestService = ( Service: Constructor, - overrides?: Overrides, + overrides: Partial = {}, ) => { - const { metadataRepository, userRepository, syncRepository } = overrides || {}; - - const accessMock = newAccessRepositoryMock(); - const loggerMock = newLoggingRepositoryMock(); - const cronMock = newCronRepositoryMock(); - const cryptoMock = newCryptoRepositoryMock(); - const activityMock = newActivityRepositoryMock(); - const auditMock = newAuditRepositoryMock(); - const albumMock = newAlbumRepositoryMock(); - const albumUserMock = newAlbumUserRepositoryMock(); - const assetMock = newAssetRepositoryMock(); - const configMock = newConfigRepositoryMock(); - const databaseMock = newDatabaseRepositoryMock(); - const eventMock = newEventRepositoryMock(); - const jobMock = newJobRepositoryMock(); - const apiKeyMock = newKeyRepositoryMock(); - const libraryMock = newLibraryRepositoryMock(); - const machineLearningMock = newMachineLearningRepositoryMock(); - const mapMock = newMapRepositoryMock(); - const mediaMock = newMediaRepositoryMock(); - const memoryMock = newMemoryRepositoryMock(); - const metadataMock = (metadataRepository || newMetadataRepositoryMock()) as Mocked< - RepositoryInterface - >; - const moveMock = newMoveRepositoryMock(); - const notificationMock = newNotificationRepositoryMock(); - const oauthMock = newOAuthRepositoryMock(); - const partnerMock = newPartnerRepositoryMock(); - const personMock = newPersonRepositoryMock(); - const processMock = newProcessRepositoryMock(); - const searchMock = newSearchRepositoryMock(); - const serverInfoMock = newServerInfoRepositoryMock(); - const sessionMock = newSessionRepositoryMock(); - const sharedLinkMock = newSharedLinkRepositoryMock(); - const stackMock = newStackRepositoryMock(); - const storageMock = newStorageRepositoryMock(); - const syncMock = (syncRepository || newSyncRepositoryMock()) as Mocked>; - const systemMock = newSystemMetadataRepositoryMock(); - const tagMock = newTagRepositoryMock(); - const telemetryMock = newTelemetryRepositoryMock(); - const trashMock = newTrashRepositoryMock(); - const userMock = (userRepository || newUserRepositoryMock()) as Mocked>; - const versionHistoryMock = newVersionHistoryRepositoryMock(); - const viewMock = newViewRepositoryMock(); + const mocks: ServiceMocks = { + access: newAccessRepositoryMock(), + logger: newLoggingRepositoryMock(), + cron: newCronRepositoryMock(), + crypto: newCryptoRepositoryMock(), + activity: newActivityRepositoryMock(), + audit: newAuditRepositoryMock(), + album: newAlbumRepositoryMock(), + albumUser: newAlbumUserRepositoryMock(), + asset: newAssetRepositoryMock(), + config: newConfigRepositoryMock(), + database: newDatabaseRepositoryMock(), + downloadRepository: newDownloadRepositoryMock(), + event: newEventRepositoryMock(), + job: newJobRepositoryMock(), + apiKey: newKeyRepositoryMock(), + library: newLibraryRepositoryMock(), + machineLearning: newMachineLearningRepositoryMock(), + map: newMapRepositoryMock(), + media: newMediaRepositoryMock(), + memory: newMemoryRepositoryMock(), + metadata: newMetadataRepositoryMock(), + move: newMoveRepositoryMock(), + notification: newNotificationRepositoryMock(), + oauth: newOAuthRepositoryMock(), + partner: newPartnerRepositoryMock(), + person: newPersonRepositoryMock(), + process: newProcessRepositoryMock(), + search: newSearchRepositoryMock(), + serverInfo: newServerInfoRepositoryMock(), + session: newSessionRepositoryMock(), + sharedLink: newSharedLinkRepositoryMock(), + stack: newStackRepositoryMock(), + storage: newStorageRepositoryMock(), + sync: newSyncRepositoryMock(), + systemMetadata: newSystemMetadataRepositoryMock(), + tag: newTagRepositoryMock(), + telemetry: newTelemetryRepositoryMock(), + trash: newTrashRepositoryMock(), + user: newUserRepositoryMock(), + versionHistory: newVersionHistoryRepositoryMock(), + view: newViewRepositoryMock(), + }; const sut = new Service( - loggerMock as ILoggingRepository as LoggingRepository, - accessMock as IAccessRepository as AccessRepository, - activityMock as RepositoryInterface as ActivityRepository, - auditMock as RepositoryInterface as AuditRepository, - albumMock as RepositoryInterface as AlbumRepository, - albumUserMock as RepositoryInterface as AlbumUserRepository, - assetMock as RepositoryInterface as AssetRepository, - configMock as RepositoryInterface as ConfigRepository, - cronMock as RepositoryInterface as CronRepository, - cryptoMock as RepositoryInterface as CryptoRepository, - databaseMock as RepositoryInterface as DatabaseRepository, - eventMock as RepositoryInterface as EventRepository, - jobMock as RepositoryInterface as JobRepository, - apiKeyMock as RepositoryInterface as ApiKeyRepository, - libraryMock as RepositoryInterface as LibraryRepository, - machineLearningMock as RepositoryInterface as MachineLearningRepository, - mapMock as RepositoryInterface as MapRepository, - mediaMock as RepositoryInterface as MediaRepository, - memoryMock as RepositoryInterface as MemoryRepository, - metadataMock as RepositoryInterface as MetadataRepository, - moveMock as RepositoryInterface as MoveRepository, - notificationMock as RepositoryInterface as NotificationRepository, - oauthMock as RepositoryInterface as OAuthRepository, - partnerMock as RepositoryInterface as PartnerRepository, - personMock as RepositoryInterface as PersonRepository, - processMock as RepositoryInterface as ProcessRepository, - searchMock as RepositoryInterface as SearchRepository, - serverInfoMock as RepositoryInterface as ServerInfoRepository, - sessionMock as RepositoryInterface as SessionRepository, - sharedLinkMock as RepositoryInterface as SharedLinkRepository, - stackMock as RepositoryInterface as StackRepository, - storageMock as RepositoryInterface as StorageRepository, - syncMock as RepositoryInterface as SyncRepository, - systemMock as RepositoryInterface as SystemMetadataRepository, - tagMock as RepositoryInterface as TagRepository, - telemetryMock as unknown as TelemetryRepository, - trashMock as RepositoryInterface as TrashRepository, - userMock as RepositoryInterface as UserRepository, - versionHistoryMock as RepositoryInterface as VersionHistoryRepository, - viewMock as RepositoryInterface as ViewRepository, + overrides.logger || (mocks.logger as As), + overrides.access || (mocks.access as IAccessRepository as AccessRepository), + overrides.activity || (mocks.activity as As), + overrides.album || (mocks.album as As), + overrides.albumUser || (mocks.albumUser as As), + overrides.apiKey || (mocks.apiKey as As), + overrides.asset || (mocks.asset as As), + overrides.audit || (mocks.audit as As), + overrides.config || (mocks.config as As as ConfigRepository), + overrides.cron || (mocks.cron as As), + overrides.crypto || (mocks.crypto as As), + overrides.database || (mocks.database as As), + overrides.downloadRepository || (mocks.downloadRepository as As), + overrides.event || (mocks.event as As), + overrides.job || (mocks.job as As), + overrides.library || (mocks.library as As), + overrides.machineLearning || (mocks.machineLearning as As), + overrides.map || (mocks.map as As), + overrides.media || (mocks.media as As), + overrides.memory || (mocks.memory as As), + overrides.metadata || (mocks.metadata as As), + overrides.move || (mocks.move as As), + overrides.notification || (mocks.notification as As), + overrides.oauth || (mocks.oauth as As), + overrides.partner || (mocks.partner as As), + overrides.person || (mocks.person as As), + overrides.process || (mocks.process as As), + overrides.search || (mocks.search as As), + overrides.serverInfo || (mocks.serverInfo as As), + overrides.session || (mocks.session as As), + overrides.sharedLink || (mocks.sharedLink as As), + overrides.stack || (mocks.stack as As), + overrides.storage || (mocks.storage as As), + overrides.sync || (mocks.sync as As), + overrides.systemMetadata || (mocks.systemMetadata as As), + overrides.tag || (mocks.tag as As), + overrides.telemetry || (mocks.telemetry as unknown as TelemetryRepository), + overrides.trash || (mocks.trash as As), + overrides.user || (mocks.user as As), + overrides.versionHistory || (mocks.versionHistory as As), + overrides.view || (mocks.view as As), ); return { sut, - mocks: { - access: accessMock, - apiKey: apiKeyMock, - cron: cronMock, - crypto: cryptoMock, - activity: activityMock, - audit: auditMock, - album: albumMock, - albumUser: albumUserMock, - asset: assetMock, - config: configMock, - database: databaseMock, - event: eventMock, - job: jobMock, - library: libraryMock, - logger: loggerMock, - machineLearning: machineLearningMock, - map: mapMock, - media: mediaMock, - memory: memoryMock, - metadata: metadataMock, - move: moveMock, - notification: notificationMock, - oauth: oauthMock, - partner: partnerMock, - person: personMock, - process: processMock, - search: searchMock, - serverInfo: serverInfoMock, - session: sessionMock, - sharedLink: sharedLinkMock, - stack: stackMock, - storage: storageMock, - systemMetadata: systemMock, - tag: tagMock, - telemetry: telemetryMock, - trash: trashMock, - user: userMock, - versionHistory: versionHistoryMock, - view: viewMock, - } as ServiceMocks, + mocks, }; }; diff --git a/web/Dockerfile b/web/Dockerfile index fc2a9e88c0..8c2e67e62e 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:22.13.1-alpine3.20@sha256:c52e20859a92b3eccbd3a36c5e1a90adc20617d8d421d65e8a622e87b5dac963 +FROM node:22.14.0-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 RUN apk add --no-cache tini USER node diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index fc5e35ce6d..f855a99c53 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -2,6 +2,8 @@ import { FlatCompat } from '@eslint/eslintrc'; import js from '@eslint/js'; import typescriptEslint from '@typescript-eslint/eslint-plugin'; import tsParser from '@typescript-eslint/parser'; +import eslintPluginSvelte from 'eslint-plugin-svelte'; +import eslintPluginUnicorn from 'eslint-plugin-unicorn'; import globals from 'globals'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -11,11 +13,12 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const compat = new FlatCompat({ baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all, }); export default [ + ...eslintPluginSvelte.configs.recommended, + eslintPluginUnicorn.configs.recommended, + js.configs.recommended, { ignores: [ '**/.DS_Store', @@ -36,15 +39,11 @@ export default [ 'coverage', ], }, - ...compat.extends( - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:svelte/recommended', - 'plugin:unicorn/recommended', - ), + ...compat.extends('plugin:@typescript-eslint/recommended'), { plugins: { '@typescript-eslint': typescriptEslint, + svelte: eslintPluginSvelte, }, languageOptions: { diff --git a/web/package-lock.json b/web/package-lock.json index 5b3373628a..cb494740b2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.126.1", + "version": "1.129.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.126.1", + "version": "1.129.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.9.8", @@ -27,6 +27,7 @@ "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", "luxon": "^3.4.4", + "qrcode": "^1.5.4", "socket.io-client": "~4.8.0", "svelte-gestures": "^5.1.3", "svelte-i18n": "^4.0.1", @@ -50,6 +51,7 @@ "@types/justified-layout": "^4.1.4", "@types/lodash-es": "^4.17.12", "@types/luxon": "^3.4.2", + "@types/qrcode": "^1.5.5", "@typescript-eslint/eslint-plugin": "^8.20.0", "@typescript-eslint/parser": "^8.20.0", "@vitest/coverage-v8": "^3.0.0", @@ -57,10 +59,10 @@ "dotenv": "^16.4.7", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.0", - "eslint-plugin-svelte": "^2.46.1", - "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-svelte": "^3.0.0", + "eslint-plugin-unicorn": "^57.0.0", "factory.ts": "^1.4.1", - "globals": "^15.14.0", + "globals": "^16.0.0", "postcss": "^8.5.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.0.0", @@ -78,13 +80,13 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.126.1", + "version": "1.129.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.13.4", + "@types/node": "^22.13.5", "typescript": "^5.3.3" } }, @@ -238,9 +240,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", "cpu": [ "ppc64" ], @@ -255,9 +257,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", "cpu": [ "arm" ], @@ -272,9 +274,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", "cpu": [ "arm64" ], @@ -289,9 +291,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", "cpu": [ "x64" ], @@ -306,9 +308,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", "cpu": [ "arm64" ], @@ -323,9 +325,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", "cpu": [ "x64" ], @@ -340,9 +342,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", "cpu": [ "arm64" ], @@ -357,9 +359,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", "cpu": [ "x64" ], @@ -374,9 +376,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", "cpu": [ "arm" ], @@ -391,9 +393,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", "cpu": [ "arm64" ], @@ -408,9 +410,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", "cpu": [ "ia32" ], @@ -425,9 +427,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", "cpu": [ "loong64" ], @@ -442,9 +444,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", "cpu": [ "mips64el" ], @@ -459,9 +461,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", "cpu": [ "ppc64" ], @@ -476,9 +478,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", "cpu": [ "riscv64" ], @@ -493,9 +495,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", "cpu": [ "s390x" ], @@ -510,9 +512,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", "cpu": [ "x64" ], @@ -527,9 +529,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", "cpu": [ "arm64" ], @@ -544,9 +546,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", "cpu": [ "x64" ], @@ -561,9 +563,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", "cpu": [ "arm64" ], @@ -578,9 +580,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", "cpu": [ "x64" ], @@ -595,9 +597,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", "cpu": [ "x64" ], @@ -612,9 +614,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", "cpu": [ "arm64" ], @@ -629,9 +631,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", "cpu": [ "ia32" ], @@ -646,9 +648,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", "cpu": [ "x64" ], @@ -663,16 +665,20 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -688,13 +694,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", - "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.5", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -703,9 +709,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", - "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -716,9 +722,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", - "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -739,37 +745,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -784,9 +759,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.20.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz", - "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", "dev": true, "license": "MIT", "engines": { @@ -794,9 +769,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", - "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -804,13 +779,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", - "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.10.0", + "@eslint/core": "^0.12.0", "levn": "^0.4.1" }, "engines": { @@ -1728,50 +1703,50 @@ } }, "node_modules/@photo-sphere-viewer/core": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.12.0.tgz", - "integrity": "sha512-uGAmIQgET9oqg6f7+a2eCtKDCq7/AMjXPMvPdG1rUsRjeoHWXEEUAI1NzMLqbpRhTwkcpP/aAlhAy+6svDFh4g==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.12.1.tgz", + "integrity": "sha512-aK+SueXdKOr5FQAMwjxswHaa2OZcpWi4tx5P4fjq1vWEDa8PtdaoSdQaAp3Csmthvd9DlfNDUb6c21fTudzM/w==", "license": "MIT", "dependencies": { "three": "^0.173.0" } }, "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.12.0.tgz", - "integrity": "sha512-J3LyJO4DnmGnZFy0+6pv5uarOCTLZj/h/Sud7L8hzB3QiNt40SICix0lYj+5JF0LDSNmWUA/xGr9EL+iqaXXiQ==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.12.1.tgz", + "integrity": "sha512-mJIArKtmM72ZQWgkXQUFdg5UAH8xTt3QpmNzMhxgO+RSowLnFov5qA3aG2DbpYc6usRiSxIwRc6LZwlCwbWVQw==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.12.0", - "@photo-sphere-viewer/video-plugin": "5.12.0" + "@photo-sphere-viewer/core": "5.12.1", + "@photo-sphere-viewer/video-plugin": "5.12.1" } }, "node_modules/@photo-sphere-viewer/resolution-plugin": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/resolution-plugin/-/resolution-plugin-5.12.0.tgz", - "integrity": "sha512-qGID7zIcZOI0e7apO1gNMDiPE4d8b9HaU7oE5Vksc9vMgwEHBhWR6yoIlm91dmqpguGP9YNYvTfCoNwTrDlTsA==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/resolution-plugin/-/resolution-plugin-5.12.1.tgz", + "integrity": "sha512-kJmw4c9zc0BAmKRprauxKOxRBA4Z+hZLKvNBUEmLlcjwp5k3p3R9tW3wphsHavfr5cSdC0BUEk4WRPs9p3oU6w==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.12.0", - "@photo-sphere-viewer/settings-plugin": "5.12.0" + "@photo-sphere-viewer/core": "5.12.1", + "@photo-sphere-viewer/settings-plugin": "5.12.1" } }, "node_modules/@photo-sphere-viewer/settings-plugin": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/settings-plugin/-/settings-plugin-5.12.0.tgz", - "integrity": "sha512-LrYsENxqqozVWl+1OelR40uvdk7mJoEDTe/ew7gKYz4qd3k1QJ4Ol/9FvcSql3DAPIzPNmumAy1fTuz7ZWg3Fw==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/settings-plugin/-/settings-plugin-5.12.1.tgz", + "integrity": "sha512-WpDT3t/4tJLkJGq7Z4MlebL3hLjJ/buXPwVpPm3j+akxtou0xhCU0pwoC/MDXb+HjchG4b4ZTpNza4jfvxLueA==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.12.0" + "@photo-sphere-viewer/core": "5.12.1" } }, "node_modules/@photo-sphere-viewer/video-plugin": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.12.0.tgz", - "integrity": "sha512-3P6J0c1fkH50s7uHWd7W27LAmyTZ5RjaqigUCGbmsTtu+crUgCfJsPUEZgysMy0micbc3ZobpUQMJJalZcAo5Q==", + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.12.1.tgz", + "integrity": "sha512-Rynni9E9u1CgQ3JnONDt0rVndaJXRtaDxMXhFgWspq1WcXiU7w1gYmctOgUBlBoZFq5ms5QYRVgBp36/+S7f4Q==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.12.0" + "@photo-sphere-viewer/core": "5.12.1" } }, "node_modules/@pkgjs/parseargs": { @@ -2117,9 +2092,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.17.1.tgz", - "integrity": "sha512-CpoGSLqE2MCmcQwA2CWJvOsZ9vW+p/1H3itrFykdgajUNAEyQPbsaSn7fZb6PLHQwe+07njxje9ss0fjZoCAyw==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.17.3.tgz", + "integrity": "sha512-GcNaPDr0ti4O/TonPewkML2DG7UVXkSxPN3nPMlpmx0Rs4b2kVP4gymz98WEHlfzPXdd4uOOT1Js26DtieTNBQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2553,21 +2528,30 @@ "version": "20.8.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.2.tgz", "integrity": "sha512-Vvycsc9FQdwhxE3y3DzeIxuEJbWGDsnrxvMADzTDF/lcdR9/K+AQIeAghTQsHtotg/q0j3WEOYS/jQgSdWue3w==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/pbf": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==" }, + "node_modules/@types/qrcode": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz", + "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/supercluster": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", @@ -2577,17 +2561,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz", - "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz", + "integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/type-utils": "8.24.0", - "@typescript-eslint/utils": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/type-utils": "8.25.0", + "@typescript-eslint/utils": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2607,16 +2591,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz", - "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz", + "integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/typescript-estree": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4" }, "engines": { @@ -2632,14 +2616,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz", - "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz", + "integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0" + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2650,14 +2634,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz", - "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz", + "integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.24.0", - "@typescript-eslint/utils": "8.24.0", + "@typescript-eslint/typescript-estree": "8.25.0", + "@typescript-eslint/utils": "8.25.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, @@ -2674,9 +2658,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz", - "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz", + "integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==", "dev": true, "license": "MIT", "engines": { @@ -2688,14 +2672,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz", - "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz", + "integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/visitor-keys": "8.24.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/visitor-keys": "8.25.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -2741,16 +2725,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz", - "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz", + "integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.24.0", - "@typescript-eslint/types": "8.24.0", - "@typescript-eslint/typescript-estree": "8.24.0" + "@typescript-eslint/scope-manager": "8.25.0", + "@typescript-eslint/types": "8.25.0", + "@typescript-eslint/typescript-estree": "8.25.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2765,13 +2749,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.24.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz", - "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==", + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz", + "integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.24.0", + "@typescript-eslint/types": "8.25.0", "eslint-visitor-keys": "^4.2.0" }, "engines": { @@ -2796,9 +2780,9 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz", - "integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.7.tgz", + "integrity": "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2819,8 +2803,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.0.5", - "vitest": "3.0.5" + "@vitest/browser": "3.0.7", + "vitest": "3.0.7" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2847,15 +2831,15 @@ } }, "node_modules/@vitest/expect": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", - "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz", + "integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, "funding": { @@ -2863,13 +2847,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", - "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz", + "integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.0.5", + "@vitest/spy": "3.0.7", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -2890,9 +2874,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", - "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz", + "integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==", "dev": true, "license": "MIT", "dependencies": { @@ -2903,38 +2887,38 @@ } }, "node_modules/@vitest/runner": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", - "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz", + "integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.0.5", - "pathe": "^2.0.2" + "@vitest/utils": "3.0.7", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", - "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz", + "integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", + "@vitest/pretty-format": "3.0.7", "magic-string": "^0.30.17", - "pathe": "^2.0.2" + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", - "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz", + "integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==", "dev": true, "license": "MIT", "dependencies": { @@ -2945,14 +2929,14 @@ } }, "node_modules/@vitest/utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", - "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz", + "integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.0.5", - "loupe": "^3.1.2", + "@vitest/pretty-format": "3.0.7", + "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" }, "funding": { @@ -3305,9 +3289,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", - "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "dev": true, "funding": [ { @@ -3344,12 +3328,13 @@ "dev": true }, "node_modules/builtin-modules": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", - "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz", + "integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3391,6 +3376,15 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -3437,9 +3431,9 @@ } }, "node_modules/chai": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz", - "integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, "license": "MIT", "dependencies": { @@ -3523,9 +3517,9 @@ } }, "node_modules/ci-info": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", - "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", + "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", "dev": true, "funding": [ { @@ -3533,6 +3527,7 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } @@ -3710,13 +3705,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.39.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz", - "integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==", + "version": "3.41.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", + "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.2" + "browserslist": "^4.24.4" }, "funding": { "type": "opencollective", @@ -3848,6 +3843,15 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", @@ -3944,6 +3948,12 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -4043,15 +4053,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-module-lexer": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", @@ -4108,9 +4109,9 @@ } }, "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4121,31 +4122,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" } }, "node_modules/escalade": { @@ -4190,22 +4191,22 @@ } }, "node_modules/eslint": { - "version": "9.20.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz", - "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==", + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", + "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.0", - "@eslint/core": "^0.11.0", - "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.20.0", - "@eslint/plugin-kit": "^0.2.5", + "@eslint/config-array": "^0.19.2", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.21.0", + "@eslint/plugin-kit": "^0.2.7", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -4250,9 +4251,9 @@ } }, "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.4.tgz", + "integrity": "sha512-/u+GQt8NMfXO8w17QendT4gvO5acfxQsAKirAt0LVxDnr2N8YLCVbregaNc/Yhp7NM128DwCaRvr8PLDfeNkQw==", "dev": true, "license": "MIT", "dependencies": { @@ -4266,9 +4267,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz", - "integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.2.tgz", + "integrity": "sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==", "dev": true, "license": "MIT", "bin": { @@ -4279,32 +4280,31 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", - "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.0.2.tgz", + "integrity": "sha512-+0QglmWNryvXXxRQKzLF3i+AreTsueCw7PBb0nGVBq+F9HoYqAjQeJ/9N6vFAtjMjK3wgsETrLVyBKPdeufN6Q==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@jridgewell/sourcemap-codec": "^1.4.15", - "eslint-compat-utils": "^0.5.1", + "@eslint-community/eslint-utils": "^4.4.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "eslint-compat-utils": "^0.6.4", "esutils": "^2.0.3", "known-css-properties": "^0.35.0", - "postcss": "^8.4.38", + "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.1.0", - "semver": "^7.6.2", - "svelte-eslint-parser": "^0.43.0" + "postcss-safe-parser": "^7.0.0", + "semver": "^7.6.3", + "svelte-eslint-parser": "^1.0.0" }, "engines": { - "node": "^14.17.0 || >=16.0.0" + "node": "^18.20.4 || ^20.18.0 || >=22.10.0" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", + "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { @@ -4314,28 +4314,28 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "56.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz", - "integrity": "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==", + "version": "57.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-57.0.0.tgz", + "integrity": "sha512-zUYYa6zfNdTeG9BISWDlcLmz16c+2Ck2o5ZDHh0UzXJz3DEP7xjmlVDTzbyV0W+XksgZ0q37WEWzN2D2Ze+g9Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "@eslint-community/eslint-utils": "^4.4.0", - "ci-info": "^4.0.0", + "@babel/helper-validator-identifier": "^7.25.9", + "@eslint-community/eslint-utils": "^4.4.1", + "ci-info": "^4.1.0", "clean-regexp": "^1.0.0", - "core-js-compat": "^3.38.1", + "core-js-compat": "^3.40.0", "esquery": "^1.6.0", - "globals": "^15.9.0", - "indent-string": "^4.0.0", - "is-builtin-module": "^3.2.1", - "jsesc": "^3.0.2", + "globals": "^15.15.0", + "indent-string": "^5.0.0", + "is-builtin-module": "^4.0.0", + "jsesc": "^3.1.0", "pluralize": "^8.0.0", - "read-pkg-up": "^7.0.1", + "read-package-up": "^11.0.0", "regexp-tree": "^0.1.27", - "regjsparser": "^0.10.0", - "semver": "^7.6.3", - "strip-indent": "^3.0.0" + "regjsparser": "^0.12.0", + "semver": "^7.7.1", + "strip-indent": "^4.0.0" }, "engines": { "node": ">=18.18" @@ -4344,33 +4344,63 @@ "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" }, "peerDependencies": { - "eslint": ">=8.56.0" + "eslint": ">=9.20.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", "dev": true, "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.1" }, "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -4388,23 +4418,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/@eslint/core": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz", - "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/eslint/node_modules/@humanwhocodes/retry": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", - "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4481,23 +4498,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", - "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", @@ -4511,24 +4511,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4573,17 +4555,31 @@ } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -4704,9 +4700,9 @@ } }, "node_modules/fabric": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.5.4.tgz", - "integrity": "sha512-X+O8G+3aDQSp3lxRekvIy/gMwtzcjAG7IvGXjb4PeUD6+nUJfSfGnaEWpni9aAcXuGz8zXhpMQQELV+4FfRTwA==", + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/fabric/-/fabric-6.6.1.tgz", + "integrity": "sha512-QrQkx6I7daFL/WdkrE8VOEiAr/ffLK36NQ0t/vNZt8P7QIXPpjT4HegjOatUW1G6vYlulX4pI1P/5NeqIgsDig==", "license": "MIT", "engines": { "node": ">=16.20.0" @@ -5038,6 +5034,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -5190,7 +5199,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -5299,9 +5307,9 @@ } }, "node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", + "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", "dev": true, "license": "MIT", "engines": { @@ -5382,10 +5390,17 @@ "optional": true }, "node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", @@ -5543,6 +5558,19 @@ "node": ">=8" } }, + "node_modules/index-to-position": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-0.1.2.tgz", + "integrity": "sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5593,12 +5621,6 @@ "tslib": "2" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -5611,15 +5633,16 @@ } }, "node_modules/is-builtin-module": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", - "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-4.0.0.tgz", + "integrity": "sha512-rWP3AMAalQSesXO8gleROyL2iKU73SX5Er66losQn9rWOWL4Gef0a/xOEOVqjWGMuR2vHG3FJ8UUmT700O8oFg==", "dev": true, + "license": "MIT", "dependencies": { - "builtin-modules": "^3.3.0" + "builtin-modules": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=18.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5905,6 +5928,19 @@ } } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5912,12 +5948,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6060,6 +6090,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/lru-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", @@ -6533,24 +6569,18 @@ } }, "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "bin": { - "semver": "bin/semver" + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/normalize-path": { @@ -6684,7 +6714,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6707,18 +6737,18 @@ } }, "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz", + "integrity": "sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" + "@babel/code-frame": "^7.22.13", + "index-to-position": "^0.1.2", + "type-fest": "^4.7.1" }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6740,7 +6770,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, "engines": { "node": ">=8" } @@ -6783,15 +6812,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" - }, "node_modules/pathe": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.2.tgz", - "integrity": "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true, "license": "MIT" }, @@ -6868,10 +6892,19 @@ "fflate": "^0.8.0" } }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", - "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -6995,19 +7028,30 @@ } }, "node_modules/postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" + "node": ">=18.0" }, "peerDependencies": { - "postcss": "^8.3.3" + "postcss": "^8.4.31" } }, "node_modules/postcss-scss": { @@ -7070,9 +7114,9 @@ } }, "node_modules/prettier": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz", - "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz", + "integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==", "dev": true, "license": "MIT", "bin": { @@ -7172,6 +7216,174 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/qrcode/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -7216,108 +7428,44 @@ "pify": "^2.3.0" } }, + "node_modules/read-package-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", + "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "dev": true, + "license": "MIT", "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7373,35 +7521,45 @@ } }, "node_modules/regjsparser": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz", - "integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~0.5.0" + "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" } }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -7674,9 +7832,9 @@ } }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "devOptional": true, "license": "ISC", "bin": { @@ -7690,8 +7848,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC", - "optional": true + "license": "ISC" }, "node_modules/set-cookie-parser": { "version": "2.6.0", @@ -7955,32 +8112,36 @@ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-exceptions": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", - "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", - "dev": true + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, + "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-license-ids": { - "version": "3.0.16", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz", - "integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==", - "dev": true + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" }, "node_modules/split-string": { "version": "3.1.0", @@ -8176,9 +8337,9 @@ } }, "node_modules/svelte": { - "version": "5.20.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.20.0.tgz", - "integrity": "sha512-04HJfFLaTwTyEKdPm3vYGdaD/8ZAHcd9SEBufq0FZNIrdzJWdM1usVdm4KIlzzDfM5+aMzio6BBhpXPoPGuMjg==", + "version": "5.20.5", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.20.5.tgz", + "integrity": "sha512-dpu2lTPVsAAgZFKpF7A9741sBCdXGogfxFU4aQeVgun7GVNCSVheTzj0FsT7g9OsLhBaMX4lKLwVIvmzQGytmQ==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -8285,20 +8446,21 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", - "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.0.0.tgz", + "integrity": "sha512-diZzpeeFhAxormeIhmRS4vXx98GG6T7Dq5y1a6qffqs/5MBrBqqDg8bj88iEohp6bvhU4MIABJmOTa0gXWcbSQ==", "dev": true, "license": "MIT", "dependencies": { - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "postcss": "^8.4.39", - "postcss-scss": "^4.0.9" + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.0", + "postcss": "^8.4.49", + "postcss-scss": "^4.0.9", + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.20.4 || ^20.18.0 || >=22.10.0" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -8312,6 +8474,33 @@ } } }, + "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/svelte-eslint-parser/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/svelte-gestures": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/svelte-gestures/-/svelte-gestures-5.1.3.tgz", @@ -9223,6 +9412,19 @@ "node": ">= 0.8.0" } }, + "node_modules/type-fest": { + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.36.0.tgz", + "integrity": "sha512-3T/PUdKTCnkUmhQU6FFJEHsLwadsRegktX3TNHk+2JJB9HlA8gp1/VXblXVDI93kSnXF2rdPx0GMbHtJIV2LPg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", @@ -9262,6 +9464,19 @@ "node": ">=0.8.0" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", @@ -9345,20 +9560,21 @@ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "node_modules/vite": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", - "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.5.1", + "esbuild": "^0.25.0", + "postcss": "^8.5.3", "rollup": "^4.30.1" }, "bin": { @@ -9436,16 +9652,16 @@ } }, "node_modules/vite-node": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", - "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz", + "integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==", "dev": true, "license": "MIT", "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", - "pathe": "^2.0.2", + "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { @@ -9496,31 +9712,31 @@ } }, "node_modules/vitest": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", - "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz", + "integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "3.0.5", - "@vitest/mocker": "3.0.5", - "@vitest/pretty-format": "^3.0.5", - "@vitest/runner": "3.0.5", - "@vitest/snapshot": "3.0.5", - "@vitest/spy": "3.0.5", - "@vitest/utils": "3.0.5", - "chai": "^5.1.2", + "@vitest/expect": "3.0.7", + "@vitest/mocker": "3.0.7", + "@vitest/pretty-format": "^3.0.7", + "@vitest/runner": "3.0.7", + "@vitest/snapshot": "3.0.7", + "@vitest/spy": "3.0.7", + "@vitest/utils": "3.0.7", + "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", - "pathe": "^2.0.2", + "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", - "vite-node": "3.0.5", + "vite-node": "3.0.7", "why-is-node-running": "^2.3.0" }, "bin": { @@ -9536,8 +9752,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.0.5", - "@vitest/ui": "3.0.5", + "@vitest/browser": "3.0.7", + "@vitest/ui": "3.0.7", "happy-dom": "*", "jsdom": "*" }, @@ -9670,6 +9886,12 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", diff --git a/web/package.json b/web/package.json index c5016640de..ad6dff891d 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.126.1", + "version": "1.129.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", @@ -38,6 +38,7 @@ "@types/justified-layout": "^4.1.4", "@types/lodash-es": "^4.17.12", "@types/luxon": "^3.4.2", + "@types/qrcode": "^1.5.5", "@typescript-eslint/eslint-plugin": "^8.20.0", "@typescript-eslint/parser": "^8.20.0", "@vitest/coverage-v8": "^3.0.0", @@ -45,10 +46,10 @@ "dotenv": "^16.4.7", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.0", - "eslint-plugin-svelte": "^2.46.1", - "eslint-plugin-unicorn": "^56.0.1", + "eslint-plugin-svelte": "^3.0.0", + "eslint-plugin-unicorn": "^57.0.0", "factory.ts": "^1.4.1", - "globals": "^15.14.0", + "globals": "^16.0.0", "postcss": "^8.5.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.0.0", @@ -83,6 +84,7 @@ "justified-layout": "^4.1.0", "lodash-es": "^4.17.21", "luxon": "^3.4.4", + "qrcode": "^1.5.4", "socket.io-client": "~4.8.0", "svelte-gestures": "^5.1.3", "svelte-i18n": "^4.0.1", diff --git a/web/src/app.css b/web/src/app.css index 1127b60624..9bc1695a8f 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -127,7 +127,7 @@ input:focus-visible { @layer utilities { .immich-form-input { - @apply rounded-xl bg-slate-200 px-3 py-3 text-sm focus:border-immich-primary disabled:cursor-not-allowed disabled:bg-gray-400 disabled:text-gray-200 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800; + @apply rounded-xl bg-slate-200 px-3 py-3 text-sm focus:border-immich-primary disabled:cursor-not-allowed disabled:bg-gray-400 disabled:text-gray-400 dark:bg-gray-600 dark:text-immich-dark-fg dark:disabled:bg-gray-800 dark:disabled:text-gray-200; } .immich-form-label { diff --git a/web/src/lib/actions/intersection-observer.ts b/web/src/lib/actions/intersection-observer.ts index edbc07e5c1..3a10074051 100644 --- a/web/src/lib/actions/intersection-observer.ts +++ b/web/src/lib/actions/intersection-observer.ts @@ -60,7 +60,7 @@ const observe = (key: HTMLElement | string, target: HTMLElement, properties: Int (entries: IntersectionObserverEntry[]) => { // This IntersectionObserver is limited to observing a single element, the one the // action is attached to. If there are multiple entries, it means that this - // observer is being notified of multiple events that have occured quickly together, + // observer is being notified of multiple events that have occurred quickly together, // and the latest element is the one we are interested in. entries.sort((a, b) => a.time - b.time); diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte index 0e39647c75..80dd29e0be 100644 --- a/web/src/lib/components/admin-page/jobs/job-tile.svelte +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -185,7 +185,7 @@ {#if !disabled && !multipleButtons && isIdle} onCommand({ command: JobCommand.Start, force: false })}> - {$t('start').toUpperCase()} + {missingText} {/if} diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index 9b4f3ffdd6..2c59f59416 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -79,8 +79,7 @@ icon: mdiLibraryShelves, title: $getJobName(JobName.Library), subtitle: $t('admin.library_tasks_description'), - allText: $t('all'), - missingText: $t('refresh'), + missingText: $t('rescan'), }, [JobName.Sidecar]: { title: $getJobName(JobName.Sidecar), @@ -135,14 +134,14 @@ [JobName.StorageTemplateMigration]: { icon: mdiFolderMove, title: $getJobName(JobName.StorageTemplateMigration), - missingText: $t('missing'), + missingText: $t('start'), description: StorageMigrationDescription, }, [JobName.Migration]: { icon: mdiFolderMove, title: $getJobName(JobName.Migration), subtitle: $t('admin.migration_job_description'), - missingText: $t('missing'), + missingText: $t('start'), }, }; @@ -170,7 +169,7 @@

- {#each jobList as [jobName, { title, subtitle, description, disabled, allText, refreshText, missingText, icon, handleCommand: handleCommandOverride }]} + {#each jobList as [jobName, { title, subtitle, description, disabled, allText, refreshText, missingText, icon, handleCommand: handleCommandOverride }] (jobName)} {@const { jobCounts, queueStatus } = jobs[jobName]}
- {#each jobNames as jobName} + {#each jobNames as jobName (jobName)}
{#if isSystemConfigJobDto(jobName)}
- {#each config.machineLearning.urls as _, i} + {#each config.machineLearning.urls as _, i (i)} {#snippet removeButton()} {#if config.machineLearning.urls.length > 1} {#snippet children({ message })} - {message} + {message} {/snippet}

diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte index 74d240a4a6..9b4aa5e934 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte @@ -73,6 +73,7 @@ filetype: 'IMG', filetypefull: 'IMAGE', assetId: 'a8312960-e277-447d-b4ea-56717ccba856', + assetIdShort: '56717ccba856', album: $t('album_name'), }; @@ -203,7 +204,7 @@

UPLOAD_LOCATION/{$user.storageLabel || $user.id}UPLOAD_LOCATION/library/{$user.storageLabel || $user.id}/{parsedTemplate()}.jpg

@@ -224,7 +225,7 @@ bind:value={selectedPreset} onchange={handlePresetSelection} > - {#each templateOptions.presetOptions as preset} + {#each templateOptions.presetOptions as preset (preset)} {/each} @@ -245,7 +246,7 @@
diff --git a/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte b/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte index 379e366df6..9d8ff51cc0 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/supported-datetime-panel.svelte @@ -19,6 +19,7 @@

{$t('date_and_time').toUpperCase()}

+

{$t('admin.storage_template_date_time_description')}

@@ -28,7 +29,7 @@

{$t('year').toUpperCase()}

    - {#each options.yearOptions as yearFormat} + {#each options.yearOptions as yearFormat, index (index)}
  • {'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}
  • {/each}
@@ -37,7 +38,7 @@

{$t('month').toUpperCase()}

    - {#each options.monthOptions as monthFormat} + {#each options.monthOptions as monthFormat, index (index)}
  • {'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}
  • {/each}
@@ -46,7 +47,7 @@

{$t('week').toUpperCase()}

    - {#each options.weekOptions as weekFormat} + {#each options.weekOptions as weekFormat, index (index)}
  • {'{{'}{weekFormat}{'}}'} - {getLuxonExample(weekFormat)}
  • {/each}
@@ -55,7 +56,7 @@

{$t('day').toUpperCase()}

    - {#each options.dayOptions as dayFormat} + {#each options.dayOptions as dayFormat, index (index)}
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • {/each}
@@ -64,7 +65,7 @@

{$t('hour').toUpperCase()}

    - {#each options.hourOptions as dayFormat} + {#each options.hourOptions as dayFormat, index (index)}
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • {/each}
@@ -73,7 +74,7 @@

{$t('minute').toUpperCase()}

    - {#each options.minuteOptions as dayFormat} + {#each options.minuteOptions as dayFormat, index (index)}
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • {/each}
@@ -82,7 +83,7 @@

{$t('second').toUpperCase()}

    - {#each options.secondOptions as dayFormat} + {#each options.secondOptions as dayFormat, index (index)}
  • {'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}
  • {/each}
diff --git a/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte b/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte index 515f2e48f0..fc8f913281 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/supported-variables-panel.svelte @@ -27,6 +27,7 @@

{$t('other').toUpperCase()}

  • {`{{assetId}}`} - Asset ID
  • +
  • {`{{assetIdShort}}`} - Asset ID (last 12 characters)
  • {`{{album}}`} - Album Name
diff --git a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte index c27df817c2..f23289d1e5 100644 --- a/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/template-settings/template-settings.svelte @@ -92,7 +92,7 @@ {/if} - {#each templateConfigs as { label, templateKey, descriptionTags, templateName }} + {#each templateConfigs as { label, templateKey, descriptionTags, templateName } (templateKey)} {$t('owner')}

- {#each album.albumUsers as { user, role }} + {#each album.albumUsers as { user, role } (user.id)}
diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index acbced70a0..cd454f515f 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -73,7 +73,7 @@

{$t('selected')}

- {#each Object.values(selectedUsers) as { user }} + {#each Object.values(selectedUsers) as { user } (user.id)} {#key user.id}
{$t('users')}
- {#each users as user} + {#each users as user (user.id)} {#if !Object.keys(selectedUsers).includes(user.id)}
- {#each sharedLinks as sharedLink} + {#each sharedLinks as sharedLink (sharedLink.id)} {/each} diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts index f8cfd447f0..40b189080f 100644 --- a/web/src/lib/components/asset-viewer/actions/action.ts +++ b/web/src/lib/components/asset-viewer/actions/action.ts @@ -19,3 +19,4 @@ export type Action = { [K in AssetAction]: { type: K } & ActionMap[K]; }[AssetAction]; export type OnAction = (action: Action) => void; +export type PreAction = (action: Action) => void; diff --git a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte index ab0da059d0..202f0e4593 100644 --- a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte @@ -1,6 +1,7 @@ + (showSelectionModal = true) }} +/> + { try { + preAction({ type: AssetAction.TRASH, asset }); await deleteAssets({ assetBulkDeleteDto: { ids: [asset.id] } }); onAction({ type: AssetAction.TRASH, asset }); diff --git a/web/src/lib/components/asset-viewer/album-list-item.svelte b/web/src/lib/components/asset-viewer/album-list-item.svelte index 43352a4904..1d7ee2971a 100644 --- a/web/src/lib/components/asset-viewer/album-list-item.svelte +++ b/web/src/lib/components/asset-viewer/album-list-item.svelte @@ -3,14 +3,25 @@ import { type AlbumResponseDto } from '@immich/sdk'; import { normalizeSearchString } from '$lib/utils/string-utils.js'; import AlbumListItemDetails from './album-list-item-details.svelte'; + import type { Action } from 'svelte/action'; + import { SCROLL_PROPERTIES } from '$lib/components/shared-components/album-selection/album-selection-utils'; interface Props { album: AlbumResponseDto; searchQuery?: string; + selected: boolean; onAlbumClick: () => void; } - let { album, searchQuery = '', onAlbumClick }: Props = $props(); + let { album, searchQuery = '', selected = false, onAlbumClick }: Props = $props(); + + const scrollIntoViewIfSelected: Action = (node) => { + $effect(() => { + if (selected) { + node.scrollIntoView(SCROLL_PROPERTIES); + } + }); + }; let albumNameArray: string[] = $state(['', '', '']); @@ -31,7 +42,10 @@ diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index 494dd94666..4e341c5743 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -65,7 +65,7 @@ widthStyle="100%" /> {#if person.isFavorite} -
+
{/if} diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index 7bf87fc67b..73c3ea7ae5 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -25,7 +25,7 @@ import AssignFaceSidePanel from './assign-face-side-panel.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { zoomImageToBase64 } from '$lib/utils/people-utils'; - import { photoViewerImgElement } from '$lib/stores/assets.store'; + import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { t } from 'svelte-i18n'; import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; @@ -221,7 +221,7 @@
{:else} - {#each peopleWithFaces as face, index} + {#each peopleWithFaces as face, index (face.id)} {@const personName = face.person ? face.person?.name : $t('face_unassigned')}